Welcome to the MCP Labs¶
Here you’ll Learn and Build Model Context Protocol (MCP) from Scratch

Introduction¶
- Welcome to the MCP Labs - a comprehensive, hands-on guide to mastering the Model Context Protocol!
- Whether you’re new to MCP or looking to deepen your understanding, this learning series will take you from fundamentals to building production-ready MCP servers.
What is MCP?¶
- The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context to Large Language Models (LLMs).
- Think of it as a universal adapter that allows AI applications to connect to various data sources, tools, and services in a consistent and scalable way.
What You’ll Learn¶
Through hands-on labs, you’ll:
- Understand MCP Architecture - Master the client-server model and protocol fundamentals
- Build MCP Servers - Create servers from scratch using TypeScript and the official SDK
- Implement Tools - Develop functions that LLMs can call to perform actions
- Expose Resources - Provide contextual data that LLMs can read and reference
- Create Prompts - Build reusable templates for common tasks
- Deploy to Production - Apply best practices for real-world applications
Learning Path¶
Who Is This For?¶
This learning series is designed for:
- Developers building AI-powered applications
- Engineers integrating LLMs with existing systems
- Technical Architects designing AI infrastructure
- DevOps Professionals deploying and maintaining MCP servers
- AI Enthusiasts wanting to understand standardized AI application development
- Anyone curious about standardized AI application development
Prerequisites¶
To get the most out of these labs, you should have:
- Basic Programming Knowledge - Familiarity with JavaScript/TypeScript
- Node.js Experience - Understanding of npm and basic Node.js concepts
- Command Line Skills - Comfortable with terminal/shell commands
- Code Editor - VS Code or similar IDEs (VS Code recommended)
Required Software¶
Before starting, ensure you have installed:
- Node.js v18 or later (download)
- npm or yarn package manager
- Git for version control
- A code editor (VS Code with TypeScript is support recommended)
Tools You’ll Use¶
Throughout the labs, you’ll work with:
- @modelcontextprotocol/sdk - Official TypeScript SDK
- MCP Inspector - Essential testing tool
- TypeScript - Type-safe server development
- JSON-RPC 2.0 - Communication protocol
Learning Approach¶
Each lab includes:
- Clear Objectives - Know what you’ll learn before you start
- Detailed Explanations - Understand the “why” behind the code
- Complete Code Examples - Working, tested code you can run
- Hands-on Exercises - Practice what you’ve learned
- Key Takeaways - Reinforce important concepts
- Additional Resources - Dive deeper on specific topics
In addition, you’ll find a complete Tasks section dedicated just to exercises and challenges to solidify your understanding.
Getting Started¶
Ready to begin your MCP journey?
Here’s how to start:
- Browse the Labs - See an overview of all available labs
- Start with Lab 1 - Begin with the fundamentals
- Complete the various labs in Order - Each lab builds on previous knowledge
- Practice & Experiment - Try variations and explore beyond examples in the Tasks section
Community & Support¶
Join the growing MCP community:
- MCP Discord - Ask questions, share projects
- GitHub Organization - Contribute to the ecosystem
- Official Documentation - Comprehensive reference
- MCP Specification - Protocol details
Why Learn MCP?¶
MCP is revolutionizing how we build AI applications:
- Universal Connectivity - One integration works across all MCP-compatible apps
- Reusability - Build once, use everywhere
- Scalability - Add new capabilities without rebuilding integrations
- Standardization - Consistent patterns and best practices
- Growing Ecosystem - Join a vibrant, expanding community
After This Course¶
Upon completion, you’ll be able to:
- Build custom MCP servers for your specific needs
- Integrate LLMs with your company’s tools and data sources
- Contribute to the MCP open source ecosystem
- Create servers that others can use
- Help others learn and adopt MCP
Explanations¶
This document provides detailed explanations of key concepts in the MCP (Model Context Protocol) server implementation, including endpoints, function calls, and RAGs (Retrieval-Augmented Generation).
Endpoints¶
General Meaning of Endpoints in This MCP Server¶
In the context of this MCP (Model Context Protocol) server, the “endpoints” refer to the API routes (URLs) that the server exposes for clients (like MCP inspectors, AI assistants, or other tools) to interact with it.
These endpoints are part of the server’s manifest, which is a metadata document that describes the server’s capabilities, transport method (e.g., “streamable-http”), and available routes.
The manifest is served at /.well-known/mcp and helps clients discover and connect to the server.
The endpoints follow RESTful conventions and support HTTP methods like GET, POST, and OPTIONS (for CORS preflight). They enable core MCP functionalities such as tool execution, resource access, prompt management, and server health checks. The server uses FastMCP (a framework for building MCP servers) and runs on port 8889 by default.
Each endpoint is implemented as a custom route in the code, often with CORS headers for browser-based clients.
Below, you’ll find explanations for each endpoint from the manifest, including its purpose, typical HTTP methods, and what it does based on the code implementation.
They are grouped logically for clarity.
Core Server and Discovery Endpoints¶
These handle basic server operations, discovery, and connection setup.
-
manifest (
/.well-known/mcp):
Serves the MCP manifest (metadata about the server, including capabilities and all endpoints). Clients use this to understand what the server supports. Handled bymcp_manifest()– returns JSON with server info, base URL, and endpoint list. -
health (
/health):
Simple health check to confirm the server is running. Returns a plain text response like “MCP Server Running”. Handled byhealth_check(). -
ping (
/ping):
Connection health check with more details. Returns JSON with status (“ok”), timestamp, and server name. Handled byping(). -
root (
/):
Root endpoint for basic server status. Similar to health, returns “MCP Server Running”. Handled byroot_health_check(). -
negotiate (
/negotiate):
Used for connection negotiation (e.g., transport setup and optional authentication via tokens). Clients send tokens here; the server responds with connection details. Handled bynegotiate()– supports proxy tokens from headers or query params. -
metadata (
/metadata):
Provides detailed server metadata, including protocol version and capabilities (e.g., support for tools, prompts). Handled bymetadata()– returns JSON with server info and feature flags. -
events (
/mcp):
The main MCP event stream endpoint for streamable HTTP transport. This is where real-time communication happens (e.g., tool calls, responses). It’s the core mount path for the FastMCP server. Handled by the FastMCP framework’s run method.
Tool-Related Endpoints¶
These manage MCP tools (functions the server exposes, like “hello” or “add”).
-
tools (
/tools):
Lists all available tools with metadata (names, descriptions, arguments). Clients use this to discover tools. Handled bytools_list()– returns JSON with tool details from the server’slist_tools()method. -
tools_execute (
/tools/execute):
Executes a single tool synchronously. Clients send the tool name and arguments; the server runs it and returns the result. Handled bytool_execute()– validates args, executes viaexecute_tool(), and tracks executions. -
tools_batch (
/tools/batch):
Executes multiple tools in a batch (array of calls). Useful for efficiency. Handled bytool_batch_execute()– processes each call and returns results. -
tools_stream (
/tools/stream):
Executes a tool with streaming responses (e.g., for long-running tasks). Returns NDJSON (newline-delimited JSON) events like “start”, “result”, and “end”. Handled bytool_stream_execute(). -
tools_history (
/tools/history):
Retrieves execution history for tools (recent runs, with optional limit). Handled bytool_history()– returns JSON with past executions fromTOOL_EXECUTIONS.
Prompt and Resource Endpoints¶
These handle reusable prompts and static resources.
-
prompts (
/prompts):
Lists available prompt templates (e.g., “code_review_prompt”). Clients can use these for structured interactions. Handled byprompts_list()– returns JSON with prompt metadata. -
resources (
/resources):
Lists available resources (e.g., server source code or info). Handled byresources_list()– returns JSON with resource URIs and descriptions.
Sampling and Roots Endpoints¶
These support advanced MCP features like LLM sampling and file system access.
-
sampling (
/sampling):
Provides LLM sampling (text generation) using Ollama. Clients send a prompt; the server generates a response. Handled bysampling()– integrates with Ollama API for completions. -
roots (
/roots):
Lists file system roots (e.g., the current working directory). Used for file-based operations. Handled byroots_list()– returns JSON with root URIs.
Custom/Ollama-Specific Endpoint¶
- ollama_status (
/ollama/status):
Checks the status of the connected Ollama instance (local LLM server). Returns model info, connection status, and available models. Handled byollama_status()– queries Ollama’s/api/tagsendpoint.
All endpoints include CORS headers for cross-origin requests and handle OPTIONS preflights. The server tracks tool executions globally for history/debugging.
Function Calls¶
What Are Function Calls?¶
In the context of MCP (Model Context Protocol) and AI systems, function calls (often referred to as “tools” in MCP terminology) are mechanisms that allow AI models or clients to invoke external functions or services dynamically.
Instead of generating plain text responses, the AI can decide to call a predefined function with specific arguments, execute it on the server, and incorporate the results into its response.
This enables more interactive, tool-augmented AI behaviors, such as performing calculations, querying databases, or interacting with APIs.
In this MCP server, tools are essentially function calls exposed via the /tools endpoints. For example, the hello tool is a function that takes a name argument and returns a greeting string.
What Do They Do?¶
Function calls allow the AI to extend its capabilities beyond static knowledge.
They enable:
- Dynamic Execution: The AI can perform real-time actions, like adding numbers or generating text via Ollama.
- Structured Interactions: Clients (e.g., an AI assistant) can call functions to retrieve data or perform tasks, then use the output in conversations.
- Modularity: Developers can add new functions without retraining the AI model.
- Safety and Control: Arguments are validated, and executions are tracked for auditing.
In MCP, tools are registered with decorators like @mcp.tool(), and clients discover them via the /tools endpoint.
How to Set Them Up¶
-
Define the Function: Write a Python function with type hints and a docstring. For example:
@mcp.tool() def my_tool(arg1: str, arg2: int = 0) -> str: """Description of what the tool does.""" # Implementation here return f"Result: {arg1} and {arg2}"- Use
@mcp.tool()to register it with FastMCP. - Arguments should have types; defaults are optional.
- Use
-
Validation and Execution: The server automatically validates arguments against the function signature (via
validate_tool_arguments) and executes it (viaexecute_tool). Results are tracked inTOOL_EXECUTIONS. -
Expose via Endpoints: Tools are listed at
/tools, executed at/tools/execute, etc. No additional setup needed beyond registration. -
Testing: Use the
/tools/historyendpoint to debug executions. Ensure the function handles errors gracefully. -
Integration with AI: Clients (e.g., via MCP inspectors) can call these functions. For LLM integration, the AI might be prompted to output function call JSON, which the client then executes.
Function calls are asynchronous if the function is a coroutine (async def).
RAGs (Retrieval-Augmented Generation)¶
What Are RAGs?¶
RAGs stand for Retrieval-Augmented Generation, a technique in AI where an LLM (Large Language Model) retrieves relevant information from external data sources before generating a response.
This improves accuracy, reduces hallucinations, and allows the model to access up-to-date or domain-specific knowledge not in its training data.
Instead of relying solely on pre-trained knowledge, RAGs “augment” generation with retrieved context.
In this MCP server context, RAGs can be implemented using resources (static data) or sampling (dynamic retrieval via Ollama).
For example, retrieving code snippets or server info to inform responses.
How Do They Work?¶
- Retrieval Phase: When a query is made, the system searches a knowledge base (e.g., documents, databases) for relevant chunks of data.
- Augmentation: Retrieved data is fed into the LLM’s prompt as context.
- Generation: The LLM generates a response based on both the query and retrieved data.
-
Key components:
- Data Sources: Could be files, APIs, or databases.
- Retriever: Searches and ranks relevant data (e.g., via embeddings or keywords).
- Generator: The LLM that produces the final output.
In MCP, resources at /resources can serve as static data sources, while sampling at /sampling can generate augmented responses.
How to Set Them Up¶
-
Define Data Sources: Use MCP resources for static data. For example:
@mcp.resource("mcp://my-data") def get_data() -> str: """Returns relevant data.""" return "Retrieved information here."- Resources are listed at
/resourcesand can be queried by URI.
- Resources are listed at
-
Implement Retrieval: For dynamic retrieval, integrate with tools or sampling. For instance, use a tool to query a database or API, then pass results to Ollama via
/sampling. -
Augment with Sampling: At
/sampling, send a prompt that includes retrieved context:- Ollama generates the response with augmentation.
-
Full RAG Pipeline:
- Client queries the server.
- Server retrieves data (e.g., via a tool or resource).
- Data is injected into a prompt.
- Sampling generates the augmented response.
-
Tools for RAG: Add tools like
search_documentsthat retrieve data. Combine with prompts for structured queries. -
Best Practices: Use embeddings (e.g., via Ollama or external services) for semantic search. Cache retrieved data for efficiency. Ensure data sources are secure and up-to-date.
RAGs enhance MCP servers by making them knowledge-aware, useful for applications like chatbots with custom data or code assistants.
Additional MCP Inspector Tabs and Configuration¶
The MCP Inspector provides various tabs that correspond to different capabilities and endpoints in your MCP server.
These tabs allow you to test and interact with the server’s features. Below, you’ll find explanations for each tab mentioned (resources, prompts, tools, ping, sampling, elicitations, roots, auth, metadata) and how to configure them in the JSON manifest within mcp02.py.
The manifest is defined in the mcp_manifest() function. It includes a "capabilities" object (boolean flags indicating support) and an "endpoints" object (URL paths). To enable or configure a feature, update these sections accordingly.
Resources Tab¶
- Purpose: Displays static data sources (e.g., files, server info) that clients can access.
- Configuration:
- Set
"resources": truein"capabilities". - Add
"resources": "/resources"in"endpoints". - Implement the
/resourcesendpoint to list available resources (e.g., URIs likemcp://code). - Register resources with
@mcp.resource("uri")decorators.
- Set
- Example: In
mcp02.py, resources likeget_code()andget_server_info()are registered and listed via/resources.
Prompts Tab¶
- Purpose: Shows reusable prompt templates for structured interactions (e.g., code review prompts).
- Configuration:
- Set
"prompts": truein"capabilities". - Add
"prompts": "/prompts"in"endpoints". - Implement the
/promptsendpoint to return a list of prompt metadata. - Register prompts with
@mcp.prompt()decorators.
- Set
- Example: Prompts like
code_review_prompt()are defined and exposed via/prompts.
Tools Tab¶
- Purpose: Lists executable functions (tools) that clients can invoke (e.g.,
hello,add). - Configuration:
- Set
"tools": truein"capabilities". - Add
"tools": "/tools"in"endpoints". - Implement
/toolsto return tool metadata frommcp.list_tools(). - Register tools with
@mcp.tool()decorators.
- Set
- Example: Tools like
hello()andadd()are registered and discoverable via/tools.
Ping Tab¶
- Purpose: Tests server connectivity and health with a simple ping.
- Configuration:
- Add
"ping": "/ping"in"endpoints". - Implement the
/pingendpoint to return JSON with status, timestamp, and server name.
- Add
- Example: The
ping()function returns{"status": "ok", ...}.
Sampling Tab¶
- Purpose: Allows LLM text generation (e.g., via Ollama) for completions.
- Configuration:
- Set
"sampling": truein"capabilities". - Add
"sampling": "/sampling"in"endpoints". - Implement
/samplingto accept prompts and return generated text.
- Set
- Example: Uses Ollama API to generate responses based on input prompts.
Elicitations Tab¶
- Purpose: Likely refers to logging or event elicitation (capturing server events/logs). In MCP, this may map to
"logging"capability for debugging. - Configuration:
- Set
"logging": truein"capabilities". - No specific endpoint needed, but ensure logging is enabled in the server framework.
- Set
- Note: If this refers to “events,” use the
/mcpendpoint for streamable HTTP events.
Roots Tab¶
- Purpose: Lists file system roots for file-based operations.
- Configuration:
- Set
"roots": truein"capabilities". - Add
"roots": "/roots"in"endpoints". - Implement
/rootsto return root URIs (e.g., current directory).
- Set
- Example: Returns
[{"uri": "file://current/dir", "name": "Current Directory"}].
Auth Tab¶
- Purpose: Handles authentication (e.g., token-based access).
- Configuration:
- Use the
/negotiateendpoint for auth negotiation. - Accept tokens via headers (e.g.,
Authorization: Bearer <token>). - In the manifest, no direct flag, but ensure
/negotiatesupports auth.
- Use the
- Example: The
negotiate()function checks for tokens and includes them in responses.
Metadata Tab¶
- Purpose: Provides server metadata (version, capabilities, protocol info).
- Configuration:
- Add
"metadata": "/metadata"in"endpoints". - Implement
/metadatato return detailed server info.
- Add
- Example: Returns JSON with
serverInfoandcapabilities.
To update the manifest in mcp02.py, edit the manifest dictionary in mcp_manifest(). For instance, to add a new capability, include it in "capabilities" and its endpoint in "endpoints". Restart the server after changes.
Creating a Personal Custom RAG¶
Retrieval-Augmented Generation (RAG) allows you to build a custom knowledge system by combining data retrieval with LLM generation.
Here’s how to create one in your MCP server context:
Step 1: Define Data Sources¶
- Static Data: Use MCP resources for fixed content (e.g., documents, code).
- Register with
@mcp.resource("mcp://my-data"). - Store data in files, databases, or variables.
- Register with
- Dynamic Data: Integrate APIs or databases for real-time retrieval.
- Create tools to query external sources (e.g., a tool that searches a vector database).
Step 2: Implement Retrieval¶
- Simple Retrieval: Use keyword search or basic queries.
- Example: A tool that reads from a JSON file or API.
- Advanced Retrieval: Use embeddings for semantic search.
- Install libraries like
sentence-transformersorfaiss. - Embed your data and queries, then find similar vectors.
- Example: Store document chunks in a vector DB, retrieve top matches for a query.
- Install libraries like
Step 3: Augment with LLM¶
- Integration: Pass retrieved data into prompts.
- Use the
/samplingendpoint or a tool likeollama_generate(). - Example Prompt:
"Using this data: {retrieved_info}. Answer: {user_query}".
- Use the
- Pipeline:
- User queries the server.
- Retrieve relevant data (via tool or resource).
- Inject data into LLM prompt.
- Generate response via sampling.
Step 4: Set Up in MCP Server¶
- Add Tools/Resources: Register retrieval functions as tools (e.g.,
@mcp.tool() def search_docs(query: str)). - Configure Endpoints: Ensure
/resources,/tools, and/samplingare enabled. - Testing: Use the Inspector to test retrieval and generation.
Best Practices¶
- Data Management: Keep data secure and up-to-date.
- Performance: Cache embeddings; use efficient search.
- Scalability: For large datasets, use external vector DBs like Pinecone or Weaviate.
-
Example Code Snippet:
This creates a personal RAG tailored to your data, enhancing AI responses with custom knowledge.
Adding Clients (Internal LLM)¶
To integrate an internal Large Language Model (LLM) as a client with your MCP server, you need to set up a client that can connect to the MCP server, discover its capabilities, and invoke tools, resources, or prompts.
This allows the LLM to augment its responses using the server’s functionalities, such as executing custom tools or retrieving data.
Prerequisites¶
-
MCP Server Running: Ensure your MCP server is running and accessible (e.g., at
http://localhost:8889). -
Client Library: Use an MCP-compatible client library. For Python, you can use libraries like
mcp-clientor integrate with frameworks like LangChain or LlamaIndex that support MCP. For other languages, check for MCP SDKs (e.g., Node.js MCP clients). -
LLM Setup: Have an internal LLM ready, such as Ollama running locally, or another model that supports tool calling (e.g., via function calling APIs).
-
Information Needed:
- Server Base URL: The full URL where the MCP server is hosted (e.g.,
http://localhost:8889). - Manifest URL: The URL to the manifest endpoint (e.g.,
http://localhost:8889/.well-known/mcp). This provides metadata about the server’s capabilities and endpoints. - Authentication Token (optional): If the server requires authentication, obtain a token (e.g., via the
/negotiateendpoint). Pass it in headers likeAuthorization: Bearer <token>. - Transport Method: Confirm the server uses “streamable-http” transport, as indicated in the manifest.
- Server Base URL: The full URL where the MCP server is hosted (e.g.,
Step-by-Step Instructions¶
-
Install Required Libraries:
- For Python: Install the MCP client library if available (e.g.,
pip install mcp-clientor similar). If using LangChain, installlangchainand MCP integrations. - For other setups: Ensure your LLM framework supports MCP (e.g., LlamaIndex has MCP connectors).
- For Python: Install the MCP client library if available (e.g.,
-
Fetch the Server Manifest:
- Make a GET request to the manifest URL to retrieve the server’s metadata.
- Example (using curl):
curl http://localhost:8889/.well-known/mcp - Parse the JSON response to understand available endpoints (e.g.,
/tools,/resources,/sampling) and capabilities (e.g.,tools: true).
-
Initialize the MCP Client:
- In your client code, create an MCP client instance and connect to the server.
- Provide the base URL and any authentication details.
-
Example in Python (pseudo-code):
-
Discover Capabilities:
- Use the client to list available tools, resources, or prompts.
- Example: Call
client.list_tools()to get tool metadata, which includes names, descriptions, and argument schemas.
-
Integrate with the Internal LLM:
- Configure the LLM to use the MCP client for tool calling.
- For LLMs that support function calling (e.g., GPT models or local models via libraries), map MCP tools to callable functions.
- Example workflow:
- When the LLM generates a response, check if it needs to call a tool (e.g., based on a prompt or decision).
- Use the MCP client to execute the tool:
result = client.call_tool("tool_name", args={"arg1": "value"}). - Feed the result back into the LLM’s context for the final response.
- For Ollama or similar local LLMs, you may need a wrapper script that handles the tool calling logic.
-
Handle Sampling or Generation:
- If the LLM needs to generate text augmented by the server, use the
/samplingendpoint via the client. - Example:
response = client.sample(prompt="Your prompt here", model="llama3.2:latest").
- If the LLM needs to generate text augmented by the server, use the
-
Test the Integration:
- Run a test query where the LLM invokes a tool (e.g., the “hello” tool).
- Verify that the client connects, executes the tool, and the LLM incorporates the result.
- Check logs on the server side (e.g., via
/tools/history) for executions.
-
Handle Errors and Authentication:
- Implement error handling for failed connections or tool executions.
- If authentication fails, renegotiate tokens via
/negotiate. - Ensure CORS and security settings allow the client to connect.
-
Advanced Setup:
- For streaming: Use the
/tools/streamendpoint for real-time tool execution. - For batch operations: Call multiple tools at once via
/tools/batch. - Integrate with prompts: Use
/promptsto retrieve structured prompts for the LLM.
- For streaming: Use the
By following these steps, your internal LLM can act as an MCP client, leveraging the server’s tools and resources to provide more capable and context-aware responses.
If using a specific LLM framework, refer to its documentation for MCP integration details.
Next Steps¶
View All Labs → - See the complete learning path
Start Lab 1 → - Begin your MCP journey!
Ready to Build the Future of AI Applications?
Let's get started!
Labs
MCP Labs - Learning Series¶

- Welcome to the Model Context Protocol (MCP) hands-on learning series!
- This comprehensive set of labs will take you from MCP fundamentals to building production-ready MCP servers.
- Here you’ll Learn and Build Model Context Protocol (MCP) from Scratch
- Whether you’re new to MCP or looking to deepen your understanding, this learning series will take you from fundamentals to building production-ready MCP servers.
What You’ll Learn¶
Through these 6 progressive labs, you’ll master:
| Topic | Description |
|---|---|
| MCP Architecture | Understanding and mastering the client-server model and core concepts |
| Server Development | Building MCP servers from scratch with TypeScript using the official SDK |
| Tools Implementation | Creating sophisticated tools that interact with external systems and developing functions that LLMs can call to perform actions |
| Resource Management | Exposing contextual data through MCP resources that LLMs can read and reference |
| Prompt Engineering | Building reusable prompt templates for common tasks |
| Production Deployment | Applying best practices for real-world applications |
Labs Overview¶
Lab 1: MCP Fundamentals¶
Get started with the basics! Learn what MCP is, why it exists, and understand its architecture and core components.
Topics:
- What is MCP and the problem it solves
- Client-server architecture
- Core capabilities: Tools, Resources, and Prompts
- MCP communication model and lifecycle
- Common use cases
Lab 2: Building Your First MCP Server¶
Build a complete, working MCP server from the ground up.
Topics:
- Project setup with Node.js and TypeScript
- Implementing the MCP protocol
- Creating your first tool
- Testing with MCP Inspector
- Connecting to Claude Desktop
Lab 3: Implementing MCP Tools¶
Master the art of creating sophisticated, production-ready tools.
Topics:
- Advanced input validation with JSON Schema
- Real-world tool examples (Weather API, File operations, Database queries)
- Returning rich content types
- Error handling patterns
- Performance optimization and caching
Lab 4: Working with MCP Resources¶
Learn to expose contextual data that LLMs can read and reference.
Topics:
- Understanding tools vs. resources
- Implementing different resource types
- Resource URI schemes and templates
- Resource subscriptions for live updates
- Combining tools and resources
Lab 5: MCP Prompts and Complete Integration¶
Complete your MCP education with prompts and production best practices.
Topics:
- Creating reusable prompt templates
- Embedding resources in prompts
- Building a complete server with all capabilities
- Production deployment and configuration
- Debugging and troubleshooting
Lab 6: K-Agent Integration¶
Implement a specialized MCP server (K-Agent) that interacts with Kubernetes clusters to provide AI-driven log collection and analysis.
Topics:
- MCP server architecture for Kubernetes
- Secure communication with Kubernetes API
- Implementing tools for pod discovery and log retrieval
- Collecting and structuring logs for LLM consumption
- Containerizing and deploying the K-Agent server
Ready to Begin?¶
Start with Lab 1: MCP Fundamentals →
Let’s build something amazing with MCP!
Lab 000 - Environment Setup¶
- Welcome to
K-AgentLabs! - In this first lab, you’ll set up your development environment with all the tools needed for the
K-Agentinfrastructure. - This lab covers
Docker,Kubernetes, and theK-Agentlabs environment container.
What you’ll learn in this workshop:¶
- Install and configure required tools (Docker, kubectl, Helm, Ollama, MCP Inspector, K-Agent etc.)
- Build and run the K-Agent labs environment (Docker container or locally)
- Verify Kubernetes cluster connectivity
- Prepare the MCP server setup
01. Prerequisites Installation¶
🐳 Docker Installation¶
# Set up the repository
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# Install Docker
sudo yum install -y docker-ce docker-ce-cli containerd.io
# Start Docker
sudo systemctl start docker
# Add user to docker group
sudo usermod -aG docker $USER
# Restart session or run:
newgrp docker
☸️ kubectl Installation¶
⚓ Helm Installation¶
🐙 Git Installation¶
🐍 Python3 Installation¶
📦 Node.js Installation¶
🤖 Ollama Installation¶
🔍 MCP Inspector Installation¶
Kubernetes Cluster Setup (minikube)¶
Kubernetes Cluster Setup (kind)¶
02. Verify Tools Installation¶
- Verify that all the tools are installed
# Verify Docker docker --version # Verify kubectl kubectl version --client # Verify Helm helm version # Verify Git git --version # Verify Python3 python3 --version # Verify Node.js node --version # Verify Ollama (if installed) ollama --version # Verify MCP Inspector mcp-inspector --version # Verify Kind (if installed) kind --version # Verify Minikube (if installed) minikube version # Verify Cluster Info (if cluster is running) kubectl cluster-info
03. Install the K-Agent¶
- The
K-Agentlabs environment is the fundamental building block for all labs. - Its an Open Source framework to write MCP tools and interact with Kubernetes clusters.
```bash curl https://raw.githubusercontent.com/kagent-dev/kagent/refs/heads/main/scripts/get-kagent | bash
Verifty K-Agent Installation¶
04. Ollama Setup¶
- Ollama will be used as our LLLM model for the labs.
- Ollama is a local LLM server that allows you to run large language models on your machine.
- Ollama supports various models, for example: Qwen and Llama.
- We will ise the
qwen3-codermodel which is optimized for coding tasks or thegpt-oosmodel.
Pulling Models (Ollama)
- Pulling models can take a while depending on your internet speed and system performance.
- You can skip this step and return to it later when needed.
Setup Ollama models¶
# Start Ollama service
ollama serve
# In a new terminal, pull the qwen3-coder model (optimized for coding tasks)
ollama pull qwen3-coder:30b
# Alternatively, pull the gpt-oos model
ollama pull gpt-oos:7b
# Verify
ollama list
Pull a Model¶
# In a new terminal, pull the qwen3-coder model (optimized for coding tasks)
ollama pull qwen3-coder:30b
# Verify
ollama list
07. Verify Setup¶
- Let’s verify your setup by running a simple test.
Task 01: Check Cluster Information¶
Exercise 2: Test Container Environment
# Start labs environment if not running
cd labs-environment
docker compose up -d
# Execute commands in container
docker exec kagent-controller bash -c "
echo '=== Environment Check ==='
echo 'Node.js:' \$(node --version)
echo 'Python:' \$(python --version)
echo 'kubectl:' \$(kubectl version --client --short)
echo 'Helm:' \$(helm version --short)
echo '========================='
"
Expected Output:
=== Environment Check ===
Node.js: v18.x.x
Python: Python 3.10.x
kubectl: Client Version: v1.28.x
Helm: v3.13.x
=========================
08. Troubleshooting¶
Container Won’t Start
If the container fails to start, check:
Kubectl Not Working in Container
If kubectl commands fail in the container:
Lab 1 - MCP Fundamentals¶
Overview¶
MCPis a game-changer for AI application development .- In this lab, you’ll learn about the Model Context Protocol (
MCP), the communication standard that enables AI assistants to interact with external tools and services. - You’ll explore MCP concepts, test simple tools, and understand the protocol structure.
-
This lab uses the MCP server included in the K-Agent labs environment.
Prerequisites
- Make sure you have completed Lab 000 - Environment Setup before starting this lab.
Learning Objectives¶
By the end of this lab, you will:
- Understand what
MCPis and why it was created - Learn the core architecture and components of
MCP - Identify the key capabilities
MCPprovides - Understand the client-server model in
MCP - Recognize common use cases for
MCP - Learn MCP server and client communication
- Testing MCP tools with stdio transport
What is MCP?¶
- The Model Context Protocol (MCP) is an open protocol that standardizes how AI applications interact with external data sources and tools.
- It defines a structured way for AI assistants (clients) to discover, call, and receive responses from tools (servers) using JSON-RPC over various transport layers.
- MCP is designed to be flexible and extensible, allowing developers to create custom tools that can be easily integrated with different AI models.
What does MCP offer?¶
-
Standardized Communication¶
AI assistants can work with any MCP-compatible tool
-
Tool Discovery¶
Clients can discover available tools from servers
-
Structured Interaction¶
Well-defined input/output schemas using JSON
-
Standardized Communication¶
AI assistants can work with any MCP-compatible tool
-
Tool Discovery¶
Clients can discover available tools from servers
-
Structured Interaction¶
Well-defined input/output schemas using JSON
-
Multiple Transports¶
Supports stdio, HTTP, WebSocket
The Problem MCP Solves¶
Before MCP, every AI application had to build custom integrations for each data source or tool it wanted to use. This led to:
- Fragmentation: Each app using different methods to connect to the same services
- Duplication: Developers rebuilding the same integrations repeatedly
- Limited scalability: Adding new integrations was time-consuming and error-prone
- Inconsistent experiences: No standard way for LLMs to interact with external systems
The MCP Solution¶
MCP provides a standardized protocol that:
- Enables universal connectivity between LLMs and data sources
- Allows one integration to work across all MCP-compatible applications
- Provides a consistent interface for accessing tools, resources, and prompts
- Supports secure, controlled access to external systems
MCP Architecture Overview¶
MCP follows a client-server architecture with clear separation of concerns:
graph TD
A[MCP Client] --> B[Ollama Module]
A --> C[IDEs & Apps]
A --> D[AI Tools]
B --> E[MCP Protocol]
C --> E
D --> E
E --> F[MCP Server]
F --> G[Exposes: Tools, Resources & Prompts]
F --> H[Connects to: APIs, Databases, Files, etc.]
K-Agent Architecture Flow¶
flowchart TD
assistant[AI Assistant] -->|MCP tool request| kagent[K-Agent MCP Server]
kagent --> toolRouter[K-Agent Tool Router]
toolRouter -->|kubeclient| k8s[Kubernetes API]
toolRouter -->|cloud sdk| clouds[Cloud Providers]
kagent -->|context updates| datastore[State & Context Store]
datastore -->|observability| observ[Logs & Metrics]
k8s -->|responses| kagent
clouds -->|responses| kagent
kagent -->|MCP response| assistant
subgraph cluster [Kubernetes Cluster]
direction LR
k8s
datastore
observ
end
MCP Components¶
MCP Server¶
-
Exposes tools/resources to clients¶
- Provides a standardized interface for tool interaction
- Allows AI assistants to discover and call tools
- Manages tool lifecycle and execution
- Handles tool errors and retries
-
Implements the MCP protocol¶
- Follows JSON-RPC 2.0 specification
- Supports multiple transport layers (stdio, HTTP, WebSocket)
- Manages tool schemas and validation
- Supports tool discovery and listing
- Provides structured responses
-
Typical MCP Server Features¶
- Runs as a standalone process or service
- Can be deployed locally or in the cloud
- Can run in containers or serverless environments
- Scales based on demand
- Monitors tool usage and performance
- Logs tool interactions for auditing
- Supports authentication and authorization
- Integrates with AI assistants and applications
- Defines tool schemas (inputs/outputs)
- Handles tool execution
-
MCP Server Notes¶
- Communicates via transport layer (stdio, HTTP, WebSocket)
- Typically runs as a local process or container
- In K-Agent, the MCP server exposes Kubernetes and cloud management tools.
- The server listens for incoming MCP requests from AI assistants and executes the requested tools.
- In our lab The server translates MCP tool calls into Kubernetes API calls or cloud service operations.
- The server returns structured responses back to the AI assistant.
- The server can also update context/state in a datastore for persistent information.
- The server is implemented using the MCP SDK, which simplifies tool definition and communication handling.
- The server supports multiple transport layers, allowing it to communicate with different types of AI assistants.
MCP Client¶
-
General MCP Client Features¶
- The application that hosts the LLM (e.g., Roo Code, VS Code, Visual Studio, etc.)
- Discovers available tools from MCP servers
- Sends tool call requests with parameters
- Receives and processes tool responses
- Manages tool invocation lifecycle
- Handles errors and retries
- Typically embedded in remote AI assistants (Claude, ChatGPT, etc.) or local models (Ollama, etc.)
- Presents server capabilities to the user/LLM
-
The Host Application¶
- The
Clientis often part of a Host Application (like VS Code, Claude Desktop, or a CLI). - The Host manages the connection to the MCP Server.
- It provides the user interface for interacting with the AI.
- It handles permissions (asking the user before running a tool).
- The
Client Capabilities¶
| Capability | Description |
|---|---|
| Sampling | The server can request the client to sample an LLM (generate text). |
| Roots | The client can tell the server which files/folders are accessible. |
| Notifications | The client can receive notifications from the server (e.g. logs, progress). |
| Context Updates | The client can send context/state updates to the server. |
| Authentication | The client can provide authentication tokens/credentials to the server. |
| Transport | The client supports multiple transport layers (stdio, HTTP, WebSocket). |
| Error Handling | The client manages errors and retries for tool calls. |
| Tool Discovery | The client can list available tools from the server. |
| Tool Invocation | The client can call tools with parameters and receive structured responses. |
| Logging | The client can log tool interactions for auditing and debugging. |
Transport Layer - MCP Communication¶
- MCP supports multiple transport mechanisms:
| Protocol | Description |
|---|---|
stdio |
Standard input/output (used in K-Agent) |
HTTP |
RESTful API communication |
WebSocket |
Real-time bidirectional communication |
gRPC |
High-performance RPC framework |
MQTT |
Lightweight messaging protocol |
Custom |
Any custom transport implementation |
MCP Communication Flow¶
sequenceDiagram
participant C as Client
participant S as Server
C->>S: 1. Discover available tools
C->>S: 2. Send tool call request (parameters)
Note over S: 3. Validate request against schema
Note over S: 4. Execute tool logic
S-->>C: 5. Return structured response
Note over C: 6. Process response & continue interaction
MCP Tool Structure¶
- An MCP tool consists of:
-
Tool Definition¶
- Metadata about the tool (name, description)
- Input schema defining parameters
- Output schema defining response structure
- Versioning information
- Dependencies and requirements
- Authentication requirements
- Rate limiting information
- Error handling strategies
-
Tool Handler¶
- Function that implements the tool’s logic
- Receives input parameters
- Performs the tool’s operation
- Returns structured output
- Handles errors and exceptions
- Logs execution details
- Manages state/context if needed
-
Tool Execution Flow¶
- Client discovers tool from server
- Client sends tool call request with parameters
- Server validates request against tool schema
- Server invokes tool handler with parameters
- Tool handler executes logic and returns response
- Server sends structured response back to client
-
Input Schema¶
- Defines expected parameters for the tool
- Uses JSON Schema format
- Specifies data types, required fields, and descriptions
- Enables validation of incoming requests
- Facilitates client-side form generation
- Supports complex nested structures
- Allows default values and constraints
- Enhances interoperability between clients and servers
Core MCP Capabilities¶
MCP servers can expose three main types of capabilities:
1. Tools¶
Functions that the LLM can call to perform actions or retrieve information.
Examples:
- Search a database
- Make an API call
- Perform calculations
- Execute system commands
Characteristics:
- Defined with JSON Schema for input validation
- Return structured results
- Can have side effects (create, update, delete operations)
Examples:¶
-
Tool Definition:
-
Tool Handler:
2. Resources¶
Contextual data that can be read by the LLM.
Examples:
- File contents
- Database records
- API responses
- Documentation
Characteristics:
- Identified by URI (Uniform Resource Identifier)
- Can be text, binary, or structured data
- Typically read-only
- Support for templates and subscriptions
3. Prompts¶
Pre-built prompt templates that users can invoke.
Examples:
- Code review templates
- Documentation generation prompts
- Analysis frameworks
- Interaction patterns
Characteristics:
- Can include embedded resources
- Support arguments for customization
- Help standardize common tasks
- Improve consistency and quality
The MCP Lifecycle¶
Understanding how MCP clients and servers interact:
1. Initialization¶
- Client connects to server via transport layer
- Handshake to establish protocol version and capabilities
- Server sends initial tool/resource/prompt listings
- Client acknowledges and prepares for interaction
2. Capability Discovery¶
- Client requests list of available tools, resources, or prompts
- Server responds with detailed descriptions
- Client presents these to the user/LLM
- Client selects tools/resources/prompts to use
3. Execution¶
- Client sends requests to invoke tools, read resources, or render prompts
- Server processes the request
- Server returns results in standardized format
- Client handles the response and continues interaction
4. Cleanup¶
- Either party can close the connection
- Graceful shutdown with notifications
MCP Communication Model¶
MCP uses three types of messages:
1. Requests¶
- Require a response
- Include a unique request ID
- Examples:
tools/list,resources/read,tools/call
2. Responses¶
- Match to requests by ID
- Contain either results or errors
- Must be sent for every request
3. Notifications¶
- One-way messages
- Don’t require responses
- Examples:
notifications/initialized,notifications/cancelled
Security Considerations¶
When working with MCP, keep these best practices in mind:
-
Authentication & Authorization
- Servers should validate requests
- Use appropriate credentials management
- Implement least-privilege access
-
Data Privacy
- Be mindful of what data is exposed
- Implement proper access controls
- Consider encryption for sensitive data
-
Rate Limiting
- Protect against abuse
- Implement appropriate throttling
- Monitor usage patterns
-
Input Validation
- Always validate tool inputs
- Sanitize user-provided data
- Prevent injection attacks
Common Use Cases¶
MCP is ideal for:
Enterprise Integration¶
- Connect LLMs to internal databases
- Access corporate knowledge bases
- Integrate with business tools (CRM, ERP, etc.)
Developer Tools¶
- File system access
- Git operations
- Database queries
- API testing and documentation
Data Analysis¶
- Query and visualize data
- Generate reports
- Perform statistical analysis
- Access multiple data sources
Productivity¶
- Calendar and email management
- Task and project tracking
- Document processing
- Automated workflows
MCP vs. Other Approaches¶
| Approach | Pros | Cons |
|---|---|---|
| Function Calling | Simple, direct | Requires custom implementation per app |
| API Integration | Flexible | No standard, duplicated effort |
| MCP | Universal standard, reusable, scalable | Requires initial setup |
Hands-On Exercise¶
Explore an MCP Server Configuration¶
Look at how an MCP server is configured in a client application (like Roo Code):
{
"mcpServers": {
"example-server": {
"command": "node",
"args": ["/path/to/server/index.js"],
"env": {
"API_KEY": "your-api-key"
}
}
}
}
Testing MCP Tools (TS)¶
Using MCP Inspector¶
MCP Inspector is a tool for testing MCP servers interactively.
# Install mcp-inspector (if not already installed)
npm install -g @modelcontextprotocol/inspector
# Start the MCP Inspector with the TS code
npx @modelcontextprotocol/inspector node ./build/index.js
MCP Inspector UI
- MCP Inspector will start a web interface at
http://localhost:6274 - You can also test tools programmatically using the examples below.
Step-by-step MCP Inspector Testing:
-
Get the Authentication Token
When you start MCP Inspector, the terminal displays: -
Copy the Authentication URL
Copy the complete URL with the token (the second line starting withhttp://) -
Open MCP Inspector in Your Browser
Paste the complete URL with the token from step 2 into your browser. You’ll be authenticated immediately. -
Configure the Server Connection
In the MCP Inspector interface:- Verify the “Transport” is set to
stdio(NOT http or streamable-http) - You’ll see a “Command” field - it should already show:
node - look for the “Argument” field - it should show:
/app/build/index.js - Click the “Connect” button
- Wait for the status to show “Connected” with a green indicator
- Verify the “Transport” is set to
-
Explore Available Tools:
- Once connected, click on the “Tools” tab at the top of the interface, and the on “List Tools” button
- You’ll see a list of available tools from your MCP server
Authentication Required
- The MCP Inspector requires authentication by default.
- Always use the URL with the token (shown in the terminal when you start the inspector), or manually enter the token in the Configuration settings.
- If you forget the token, restart the MCP Inspector to generate a new one.
Disabling Authentication (Development Only)
Y* ou can disable authentication by setting the DANGEROUSLY_OMIT_AUTH=true environment variable:
Keep MCP Inspector Running
- Make sure the MCP Inspector command (
npx @modelcontextprotocol/inspector node /app/build/index.js) is still running in your terminal. - If the connection fails or you see errors, restart the command in the container.
Interactive Testing
- The MCP Inspector provides a user-friendly web interface to test your MCP server without writing code.
- This is perfect for debugging and understanding how MCP tools work before integrating them with AI assistants.
Understanding MCP Inspector Output
- The Inspector displays tool results in a readable format.
- Internally, MCP uses JSON-RPC 2.0 protocol with structured responses, but the UI shows you the human-readable content.
- For JSON view, see the “History” section below the UI
Deep Dive: MCP Base Components (Python Focus)¶
Now let’s examine the essential building blocks of MCP and how they’re implemented in Python. We’ll focus on the Python implementation since it’s often clearer and more accessible than TypeScript.
1. MCP Server Framework (FastMCP)¶
The foundation of any MCP server is the server framework. In Python, we use FastMCP:
from mcp.server.fastmcp import FastMCP
# Create the MCP server instance
mcp = FastMCP("my-mcp-server", port=8889)
What it does:
- Provides the HTTP server infrastructure
- Handles JSON-RPC communication
- Manages tool, resource, and prompt registration
- Implements the MCP protocol handshake
What’s inside:
- HTTP request handlers for all MCP endpoints
- Tool execution engine
- Resource serving system
- Prompt template management
2. Tool Registration Decorator¶
Tools are the core functionality exposed by MCP servers:
@mcp.tool()
def hello(name: str) -> str:
"""Returns a friendly greeting message"""
return f"Hello, {name}! Welcome to K-Agent Labs."
What it produces:
- A callable function registered with the MCP server
- JSON Schema for input validation
- Metadata for client discovery
What’s inside:
- Function signature inspection
- Automatic parameter validation
- Execution tracking and logging
3. Resource Handlers¶
Resources provide read-only data access:
@mcp.resource("mcp://server-info")
def get_server_info() -> str:
"""Returns information about this MCP server"""
return """K-Agent MCP Server
Version: 0.1.0
Capabilities:
- Tools: hello, add
- Prompts: code_review_prompt, debug_prompt
- Resources: code, server-info
"""
What it produces:
- A resource accessible via a URI
- Content generation logic
- MIME type specification
- URI-addressable data endpoints
- Structured metadata for discovery
- MIME type information
4. Prompt Templates¶
Reusable prompt templates for consistent interactions:
@mcp.prompt()
def code_review_prompt(code: str, language: str = "python") -> str:
"""Generate a code review prompt for the given code"""
# Create the prompt template with proper markdown formatting
template = f"""Please review this {language} code and provide feedback:
``` + language + f"""
""" + code + f"""
Focus on:
- Code quality and best practices
- Potential bugs or issues
- Performance improvements
- Security concerns
"""
return template
What it produces:
- A prompt template registered with the MCP server
- Argument definitions for dynamic rendering
- Metadata for client discovery
- Parameterized prompt templates
- Standardized interaction patterns
- Consistent output formatting
Example output when called:
Please review this python code and provide feedback:
```python
def hello(): pass
```
Focus on:
- Code quality and best practices
- Potential bugs or issues
- Performance improvements
- Security concerns
5. Transport Layer (HTTP Routes)¶
Custom routes handle the MCP protocol communication:
@mcp.custom_route("/.well-known/mcp", methods=["GET", "OPTIONS"])
async def mcp_manifest(request: Request) -> JSONResponse:
manifest = {
"name": "kagent-mcp-server",
"version": "0.1.0",
"capabilities": {
"tools": True,
"prompts": True,
"resources": True,
"sampling": True
}
}
return JSONResponse(manifest)
What it does:
- Implements MCP protocol endpoints
- Handles client-server negotiation
- Provides capability discovery
MCP Method Flow with Echo Commands¶
Let’s trace through the Python methods called during a typical MCP interaction, with echo commands to show what’s happening:
# 1. Server Initialization
def main():
print("🔧 Initializing MCP server...")
mcp = FastMCP("kagent-mcp-server", port=8889)
print("✅ Server instance created with port 8889")
# Register tools
print("🛠️ Registering tools...")
@mcp.tool()
def hello(name: str) -> str:
print(f"👋 Executing hello tool for {name}")
return f"Hello, {name}!"
print("✅ Tool 'hello' registered")
# Start the server
print("🚀 Starting MCP server...")
mcp.run(transport="streamable-http", mount_path="/mcp")
print("✅ Server running on http://localhost:8889")
# 2. Client Connection Process
async def connect_to_server():
print("🔗 Connecting to MCP server...")
# Get manifest
print("📋 Fetching server manifest...")
manifest = await client.get(f"{base_url}/.well-known/mcp")
print(f"✅ Got manifest: {manifest['name']} v{manifest['version']}")
# Negotiate connection
print("🤝 Negotiating connection...")
negotiate_response = await client.get(f"{base_url}/negotiate")
print(f"✅ Negotiation complete: {negotiate_response['transport']}")
# List tools
print("📋 Listing available tools...")
tools = await client.post(f"{base_url}/tools")
print(f"✅ Found {len(tools['tools'])} tools")
return True
# 3. Tool Execution Flow
async def execute_tool_flow():
print("🎯 Executing tool 'hello'...")
# Validate arguments
print("🔍 Validating tool arguments...")
# (validation logic here)
print("✅ Arguments valid")
# Execute tool
print("⚡ Calling tool function...")
result = await execute_tool("hello", {"name": "Alice"})
print(f"✅ Tool executed successfully: {result['result']}")
# Track execution
print("📊 Recording execution metrics...")
# (tracking logic here)
print("✅ Execution tracked")
return result
# 4. Resource Access Flow
async def access_resource_flow():
print("📖 Accessing resource...")
# Resolve resource URI
print("🔗 Resolving resource URI...")
# (URI resolution logic)
print("✅ Resource resolved")
# Fetch resource content
print("📥 Fetching resource content...")
content = await get_resource_content("mcp://server-info")
print(f"✅ Resource content retrieved ({len(content)} chars)")
return content
# 5. Prompt Usage Flow
async def use_prompt_flow():
print("📝 Using prompt template...")
# Get prompt template
print("🔍 Finding prompt template...")
template = await get_prompt_template("code_review_prompt",
code="def test(): pass")
print(f"✅ Template retrieved ({len(template)} chars)")
# Render with arguments
print("🎨 Rendering prompt with arguments...")
final_prompt = template # Already rendered
print("✅ Prompt rendered")
# Send to LLM
print("🤖 Sending to LLM for processing...")
response = await sample_llm(final_prompt)
print(f"✅ LLM response received ({len(response)} chars)")
return response
Method Call Order:
FastMCP.__init__()- Server initialization@mcp.tool()decorator - Tool registrationmcp.run()- Start HTTP server/.well-known/mcpGET - Client discovery/negotiateGET - Connection negotiation/toolsPOST - Tool discovery/tools/executePOST - Tool execution/resourcesGET - Resource discovery/samplingPOST - LLM interaction
What Realizes MCP¶
What Makes MCP Work¶
MCP is realized through several key mechanisms:
1. Protocol Standardization¶
- JSON-RPC 2.0 as the communication protocol
- HTTP transport for reliable message delivery
- Capability negotiation during connection establishment
- Structured error handling and response formatting
2. Component Integration¶
- Server frameworks (FastMCP, MCP SDK) that implement the protocol
- Client libraries that know how to communicate with servers
- Tool execution engines that safely run server-provided functions
- Resource resolution systems that handle URI-based data access
3. Security Boundaries¶
- Process isolation between client and server
- Input validation using JSON Schema
- Access control through authentication tokens
- Rate limiting and abuse prevention
What’s Still Missing¶
While MCP provides a solid foundation, some aspects are still evolving:
1. Advanced Authentication¶
- OAuth 2.0 integration patterns
- Role-based access control (RBAC)
- Token refresh mechanisms
2. Streaming and Real-time Updates¶
- Server-sent events for live data
- WebSocket support for bidirectional streaming
- Real-time resource subscriptions
3. Performance Optimization¶
- Connection pooling
- Caching strategies
- Batch operation optimizations
4. Enterprise Features¶
- Audit logging and compliance
- Multi-tenant isolation
- Service mesh integration
Extra Value MCP Provides¶
Beyond basic integration, MCP adds significant value:
1. Developer Experience¶
- Consistent APIs across different tools and services
- Auto-discovery of capabilities
- Type safety through schema validation
- Rich tooling (debuggers, inspectors, documentation)
2. Operational Benefits¶
- Centralized management of AI integrations
- Version compatibility checking
- Health monitoring and metrics
- Graceful degradation when services are unavailable
3. Security Advantages¶
- Controlled access to external systems
- Audit trails of AI actions
- Input sanitization and validation
- Isolation between different integrations
4. Scalability Features¶
- Horizontal scaling of MCP servers
- Load balancing across multiple instances
- Caching layers for performance
- Circuit breakers for resilience
Core Primitive Operations¶
Let’s examine the fundamental MCP operations that make everything work:
1. wellknown/mcp - Server Discovery¶
Location: /.well-known/mcp endpoint
When it happens: During initial client connection
How it works:
@mcp.custom_route("/.well-known/mcp", methods=["GET"])
async def mcp_manifest(request: Request) -> JSONResponse:
manifest = {
"name": "kagent-mcp-server",
"version": "0.1.0",
"capabilities": {
"tools": True,
"prompts": True,
"resources": True,
"sampling": True
},
"endpoints": {
"negotiate": "/negotiate",
"tools": "/tools",
"resources": "/resources",
"sampling": "/sampling"
}
}
return JSONResponse(manifest)
Outcome: Client learns server capabilities and available endpoints
2. negotiate - Connection Establishment¶
Location: /negotiate endpoint
When it happens: After manifest discovery, before using server
How it works:
@mcp.custom_route("/negotiate", methods=["GET", "POST"])
async def negotiate(request: Request) -> JSONResponse:
token = request.headers.get("x-proxy-token")
response = {
"transport": "streamable-http",
"url": f"{request.url.scheme}://{request.host}/mcp"
}
if token:
response["proxy_token"] = token
return JSONResponse(response)
Outcome: Establishes authenticated connection parameters
3. tool/call - Tool Execution¶
Location: /tools/execute endpoint
When it happens: When client wants to execute a tool function
How it works:
@mcp.custom_route("/tools/execute", methods=["POST"])
async def tool_execute(request: Request) -> JSONResponse:
body = await request.json()
tool_name = body.get("tool")
arguments = body.get("arguments", {})
# Validate and execute
result = await execute_tool(tool_name, arguments)
return JSONResponse(result)
Outcome: Executes server-side function and returns structured result
4. resource/read - Resource Access¶
Location: /resources endpoint (for listing) or direct URI resolution
When it happens: When client needs to access data resources
How it works:
@mcp.resource("mcp://server-info")
def get_server_info() -> str:
return "Server information content"
# Or via endpoint:
@mcp.custom_route("/resources", methods=["GET"])
async def resources_list(request: Request) -> JSONResponse:
resources = [
{
"uri": "mcp://server-info",
"name": "Server Information",
"mimeType": "text/plain"
}
]
return JSONResponse({"resources": resources})
Outcome: Provides access to structured data through URI-based addressing
5. sampling - LLM Integration¶
Location: /sampling endpoint
When it happens: When client needs LLM processing
How it works:
@mcp.custom_route("/sampling", methods=["POST"])
async def sampling(request: Request) -> JSONResponse:
body = await request.json()
prompt = body.get("prompt")
# Call LLM API
response = await call_llm_api(prompt)
return JSONResponse({
"completion": response,
"model": "llama3.2:latest"
})
Outcome: Enables AI processing through standardized interface
Building Your Own MCP Components¶
Step-by-Step: Creating a Basic Tool¶
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-basic-server")
@mcp.tool()
def calculate_area(length: float, width: float) -> str:
"""Calculate the area of a rectangle"""
area = length * width
return f"The area of a rectangle with length {length} and width {width} is {area}"
@mcp.resource("mcp://math-constants")
def get_math_constants() -> str:
"""Returns common mathematical constants"""
return """
Pi: 3.14159
E: 2.71828
Golden Ratio: 1.61803
"""
if __name__ == "__main__":
mcp.run()
What this produces: - A tool that can be called by MCP clients - A resource that can be read by clients - A complete MCP server ready to run
What’s inside each component: - Tool: Function + JSON schema + metadata - Resource: URI + content generator + MIME type - Server: HTTP handlers + protocol implementation + capability management
Key Takeaways¶
✅ MCP consists of server frameworks, tool decorators, resource handlers, and transport layers
✅ Python methods follow a clear execution flow: init → register → run → handle requests
✅ MCP is realized through protocol standardization, component integration, and security boundaries
✅ Core primitives handle discovery (wellknown/mcp), connection (negotiate), execution (tool/call), and data access (resource/read)
✅ Each step produces structured outputs with clear interfaces and validation
✅ Extra value comes from consistency, security, scalability, and developer experience
Next Steps¶
In Lab 2, you’ll get hands-on experience by building your first MCP server from scratch. You’ll learn:
- Setting up the development environment
- Creating a basic MCP server structure
- Implementing the initialization handshake
- Testing your server with an MCP client
Additional Resources¶
- MCP Official Documentation
- MCP Specification
- MCP GitHub Repository
- JSON-RPC 2.0 Specification
- FastMCP Python Library
Ready to build your first MCP server? Continue to Lab 2!
Lab 2: Building Your First MCP Server¶
Overview¶
Now that you understand the fundamentals of MCP, it’s time to get hands-on!
In this lab, you’ll build a complete, working MCP server from scratch. You’ll learn how to set up the project, implement the core protocol, and connect it to an MCP client.
Learning Objectives¶
By the end of this lab, you will:
- Set up a Node.js / TypeScript project for
MCPdevelopment - Implement the
MCPinitialization handshake - Create a basic server structure using the
MCPSDK - Test your server with a real
MCPclient - Understand the request/response lifecycle
- Debug and troubleshoot common issues
Prerequisites¶
Before starting, ensure you have:
- Node.js (v18 or later) installed
- npm or yarn package manager
- A code editor (VS Code recommended)
- Basic understanding of JavaScript / TypeScript
- Completed Lab 1 - MCP Fundamentals
To verify your prerequisites are installed correctly, run the following commands:
# Check Node.js version
node --version
# Check yarn version
yarn --version
# Check npm version
npm --version
# Check JavaScript version (via Node.js V8 engine)
node -e "console.log('V8 version:', process.versions.v8)"
# Check VS Code version
code --version
Project Setup¶
Step 1: Initialize Your Project¶
-
Create a new directory for your
MCPserver, navigate into it, and initialize a new Node.js project:
Step 2: Install Dependencies¶
-
Install the MCP SDK and TypeScript dependencies using npm and build tool:
-
To verify TypeScript installation:
Step 3: Configure TypeScript¶
-
Create a
tsconfig.jsonfile:{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }
Step 4: Update package.json¶
-
Replace (copy / paste) the entire content of the file
package.json, located inside the previously createdmy-first-mcp-serverdirectory, with the following content to add scripts and set module type:
Building the MCP Server¶
Step 5: Create the Server Structure¶
- Create a directory named
srcinsidemy-first-mcp-server. -
Create a file named
index.tsinside thesrcdirectory and fill it with the following basic server skeleton code (copy / paste inside the file):#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; /** * Create an MCP server with core capabilities */ class MyFirstMCPServer { private server: Server; constructor() { this.server = new Server( { name: "my-first-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); this.setupHandlers(); this.setupErrorHandling(); } /** * Set up request handlers */ private setupHandlers(): void { // Handler for listing available tools this.server.setRequestHandler( ListToolsRequestSchema, async () => ({ tools: [ { name: "hello_world", description: "Returns a friendly greeting message", inputSchema: { type: "object", properties: { name: { type: "string", description: "The name to greet", }, }, required: ["name"], }, }, ], }) ); // Handler for calling tools this.server.setRequestHandler( CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === "hello_world") { const userName = args?.name as string; if (!userName) { throw new Error("Name parameter is required"); } return { content: [ { type: "text", text: `Hello, ${userName}! Welcome to your first MCP server! 🎉`, }, ], }; } throw new Error(`Unknown tool: ${name}`); } ); } /** * Set up error handling */ private setupErrorHandling(): void { this.server.onerror = (error) => { console.error("[MCP Error]", error); }; process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } /** * Start the server */ async start(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("My First MCP Server running on stdio"); } } /** * Main entry point */ async function main() { const server = new MyFirstMCPServer(); await server.start(); } main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });
Understanding the Code¶
Let’s break down the key components:
1. Server Initialization¶
this.server = new Server(
{
name: "my-first-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
- Defines server metadata (name and version)
- Declares capabilities (in this case, the server supports tools)
- This information is sent during the initialization handshake
2. List Tools Handler¶
- Responds to
tools/listrequests from clients - Returns an array of tool definitions
- Each tool has: name, description and inputSchema (JSON Schema)
3. Call Tool Handler¶
this.server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
// Tool execution logic
}
);
- Responds to
tools/callrequests - Receives tool name and arguments
- Returns tool results in a standardized format
4. Transport Layer¶
- Uses stdio (standard input/output) for communication
- Server runs as a subprocess of the client
- All
MCPmessages flow through stdin/stdout
Testing Your Server¶
Testing with the MCP Inspector¶
-
The MCP Inspector is a web-based tool for testing MCP servers. Run these commands from your
my-first-mcp-serverdirectory: -
This will open a web interface where you can:
- See the server’s capabilities
- List available tools
- Call tools with test inputs
- View request/response messages
How to Test:¶
-
Open the Inspector: After running the command above, your default web browser should automatically open to the MCP Inspector interface (typically at
http://localhost:3000or similar). -
Connect to the Server: Look for a “Connect” button and click it. You should see a “Connected” status indicator. If the server fails to connect, check the terminal for error messages.
-
List Tools: Click on the “Tools” tab and then the “List Tools” button. You should see your
hello_worldtool appear in the list with its description and input schema. This confirms the server connection and capabilities. -
Call a Tool: In the “Tools” tab, select the
hello_worldtool from the dropdown. Enter a name in the “name” field and click “Run Tool”. -
View Results: The response should appear below, showing the greeting message. Check the logs or response sections for any errors or debug information.
-
Test Error Handling: Try calling the tool without any input in the “name” field to see how your server handles errors.
-
Monitor Messages: Check the response areas in the Tools tab or other sections to see details about the JSON-RPC messages being exchanged.
The Request - Response Lifecycle¶
Here’s what happens when a tool is called:
sequenceDiagram
participant Client
participant Server
Client->>Server: tools/list request
Note over Server: Process request:<br/>- Match handler<br/>- Execute handler function
Server-->>Client: tools/list response<br/>(list of available tools)
Client->>Server: tools/call request<br/>{ name: "hello_world",<br/>arguments: { name: "Alice" } }
Note over Server: Process request:<br/>- Validate tool name<br/>- Validate arguments<br/>- Execute tool logic<br/>- Format response
Server-->>Client: tools/call response<br/>{ content: [...] }
Error Handling Best Practices¶
Proper error handling is crucial for MCP servers!
Here’s how to implement robust error handling, step by step:
1. Validate Inputs¶
Why: Always validate input parameters before processing to prevent runtime errors and provide clear feedback to clients.
Step-by-step implementation:
-
Check for required parameters:
-
Validate parameter types:
-
Combine validation checks:
-
Validate against schema constraints:
2. Provide Helpful Error Messages¶
Why: Clear error messages help developers understand what went wrong and how to fix it.
Step-by-step implementation:
-
Include the problematic value in the error:
-
Suggest available alternatives:
-
Provide context about what was expected:
-
Include parameter names for clarity:
3. Handle Async Errors¶
Why: Asynchronous operations can fail, and these errors need to be caught and handled properly.
Step-by-step implementation:
-
Wrap async operations in try-catch:
-
Extract error information safely:
-
Preserve original error context:
-
Handle different error types appropriately:
try { const result = await someAsyncOperation(); return { content: [{ type: "text", text: result }] }; } catch (error) { if (error.code === 'ENOTFOUND') { throw new Error('Network connection failed. Please check your internet connection.'); } else if (error.code === 'ETIMEDOUT') { throw new Error('Operation timed out. Please try again.'); } else { throw new Error(`Operation failed: ${error.message}`); } }
4. Log to stderr¶
Why: MCP protocol uses stdout for communication. Logging to stdout can break the protocol.
Step-by-step implementation:
-
Use console.error() for all logging:
-
Structure your log messages:
-
Log at appropriate levels:
// Debug information console.error("[DEBUG] Server initialized with capabilities:", capabilities); // Info for normal operations console.error("[INFO] Tool executed successfully"); // Warnings for potential issues console.error("[WARN] Using default value for optional parameter"); // Errors for failures console.error("[ERROR] Tool execution failed:", error); -
Include timestamps for debugging:
Important
Use console.error() for logging, not console.log(). Your MCP server must not write to stdout for logging purposes, as
stdout is reserved for MCP protocol messages!
Common Issues and Solutions¶
Issue 1: Server Not Connecting¶
-
Symptoms: Client doesn’t see the server or times out
-
Solutions:
- Check that the command path is absolute
- Verify Node.js is in the PATH
- Look at client logs for connection errors
- Ensure the server starts without crashing
Issue 2: Tools Not Appearing¶
-
Symptoms: Server connects but no tools are listed
-
Solutions:
- Verify
capabilities.toolsis declared in server initialization - Check that
ListToolsRequestSchemahandler is registered - Ensure the handler returns the correct format
- Restart the client after code changes
- Verify
Issue 3: Tool Execution Fails¶
-
Symptoms: Tool appears but fails when called
-
Solutions:
- Validate input arguments match the schema
- Check for typos in tool names
- Add debug logging to see what’s received
- Ensure return format matches
MCPspecification
Issue 4: Server Crashes¶
-
Symptoms: Server exits unexpectedly
-
Solutions:
- Add try-catch blocks around async code
- Check for unhandled promise rejections
- Validate all external data
- Add process error handlers
Extending Your Server: Hands-On Exercises¶
Now that you have a working server, try these exercises:
Exercise 1: Add a Calculator Tool¶
-
Create a tool that performs basic math operations:
{ name: "calculate", description: "Performs basic math operations", inputSchema: { type: "object", properties: { operation: { type: "string", enum: ["add", "subtract", "multiply", "divide"], description: "The operation to perform" }, a: { type: "number", description: "First number" }, b: { type: "number", description: "Second number" } }, required: ["operation", "a", "b"] } }
Exercise 2: Add an Echo Tool¶
-
Create a tool that returns whatever text it receives:
Exercise 3: Add Logging¶
-
Enhance your server with structured logging:
Key Takeaways¶
✅ MCP servers are built using the @modelcontextprotocol/sdk package
✅ The SDK handles protocol details, letting you focus on business logic
✅ Servers communicate via stdio, HTTP, or custom transports
✅ Tools are defined with JSON Schema for type safety
✅ Error handling and validation are critical for reliability
✅ The MCP Inspector is invaluable for development and testing
✅ Always log to stderr, never stdout (reserved for protocol)
Next Steps¶
In Lab 3, you’ll dive deeper into implementing sophisticated MCP tools.
-
You’ll learn:
- Advanced input validation techniques
- Returning rich content types (text, images, embedded resources)
- Implementing async operations and long-running tasks
- Error handling patterns
- Tool composition and dependencies
Additional Resources¶
- MCP TypeScript SDK Documentation
- MCP Inspector Tool
- JSON Schema Reference
- MCP Protocol Specification
Troubleshooting Checklist¶
-
Before moving on, verify:
- Your server builds without TypeScript errors
- The server starts and logs “running on stdio”
- The MCP Inspector can connect to your server
- Tools appear in the inspector’s tool list
- You can successfully call the
hello_worldtool - Error messages are helpful and informative
Congratulations! You’ve built your first MCP server! Ready for more? Continue to Lab 3!
Lab 3: Implementing MCP Tools¶
Overview¶
In Lab 2, you created a basic MCP server with a simple “hello world” tool. Now it’s time to level up!
In this lab, you’ll master the art of creating sophisticated, production-ready MCP tools that can handle complex inputs, perform real-world operations, and return rich content types.
Learning Objectives¶
By the end of this lab, you will:
- Design robust tool schemas with advanced validation
- Implement tools that interact with external systems (APIs, databases, file systems)
- Return multiple content types (text, images, resources)
- Handle errors gracefully with detailed feedback
- Implement async operations and streaming responses
- Apply best practices for tool composition
- Test tools thoroughly with various edge cases
Prerequisites¶
- Completed Lab 2 - Building Your First MCP Server
- Understanding of async/await in JavaScript/TypeScript
- Basic knowledge of REST APIs and JSON
- Node.js development environment set up
Understanding Tool Design¶
What Makes a Great MCP Tool?¶
A well-designed tool should be:
- Single Purpose: Does one thing well
- Self-Descriptive: Clear name and description
- Well-Validated: Comprehensive input schema
- Error-Resilient: Handles failures gracefully
- Efficient: Returns results quickly
- Composable: Can work with other tools
Tool Anatomy¶
Every MCP tool consists of the following key components:
{
name: "tool_name", // Unique identifier
description: "What it does", // Clear explanation for LLM
inputSchema: { // JSON Schema for validation
type: "object",
properties: { ... },
required: [ ... ]
}
}
Advanced Input Schema Design¶
Example 1: File Search Tool¶
Key Features:
- Pattern validation using
regex - Default values for optional parameters
- Min/max constraints for numbers
- Clear descriptions for the LLM
{
name: "search_files",
description: "Search for files in a directory using glob patterns",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Directory path to search in",
pattern: "^(/|\\./|\\.\\./).*" // Must be absolute or relative path
},
pattern: {
type: "string",
description: "Glob pattern (e.g., '*.ts', '**/*.json')",
default: "*"
},
recursive: {
type: "boolean",
description: "Search subdirectories",
default: false
},
maxResults: {
type: "number",
description: "Maximum number of results to return",
minimum: 1,
maximum: 1000,
default: 100
}
},
required: ["path"]
}
}
Example 2: API Request Tool¶
Key Features:
- Format validation (
uri) - Enum constraints
- Nested objects with additional properties
- Practical defaults
{
name: "make_api_request",
description: "Make HTTP requests to REST APIs",
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "Full URL to request",
format: "uri"
},
method: {
type: "string",
description: "HTTP method",
enum: ["GET", "POST", "PUT", "DELETE", "PATCH"],
default: "GET"
},
headers: {
type: "object",
description: "HTTP headers as key-value pairs",
additionalProperties: {
type: "string"
}
},
body: {
type: "string",
description: "Request body (JSON string for POST/PUT)"
},
timeout: {
type: "number",
description: "Request timeout in milliseconds",
minimum: 100,
maximum: 30000,
default: 5000
}
},
required: ["url", "method"]
}
}
Implementing Real-World Tools¶
Tool 1: Weather Information with Ollama¶
Goal: Create a production-ready weather tool that uses Ollama (local AI) to generate weather information, handles errors gracefully, and returns formatted information.
Complete Weather Tool Implementation with Ollama¶
Here is the complete src/index.ts file with the Ollama-based weather tool added. Copy this entire code and overwrite your existing src/index.ts file:
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
/**
* Create an MCP server with core capabilities
*/
class MyFirstMCPServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: "my-first-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
/**
* Set up request handlers
*/
private setupHandlers(): void {
// Handler for listing available tools
this.server.setRequestHandler(
ListToolsRequestSchema,
async () => ({
tools: [
{
name: "get_weather",
description: "Get current weather information for a city using AI",
inputSchema: {
type: "object",
properties: {
city: {
type: "string",
description: "City name (e.g., 'London', 'New York')"
},
units: {
type: "string",
description: "Temperature units",
enum: ["celsius", "fahrenheit"],
default: "celsius"
}
},
required: ["city"]
}
},
{
name: "hello_world",
description: "Returns a friendly greeting message",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "The name to greet",
},
},
required: ["name"],
},
},
],
})
);
// Handler for calling tools
this.server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
const { name, arguments: args } = request.params;
if (name === "get_weather") {
try {
// Extract and validate parameters
const city = args.city as string;
const units = (args.units as string) || "celsius";
if (!city || city.trim().length === 0) {
throw new Error("City name cannot be empty");
}
// Use Ollama to generate weather information
const prompt = `Generate realistic current weather information for ${city}.
Return ONLY a JSON object with this exact structure:
{
"name": "${city}",
"sys": {"country": "XX"},
"main": {"temp": 20.5, "feels_like": 22.1, "humidity": 65},
"weather": [{"description": "clear sky"}],
"wind": {"speed": 3.2}
}
Use realistic weather data appropriate for the location. Temperature should be in Celsius. Choose an appropriate 2-letter country code for the city. Make the weather description realistic for the location and season.`;
// Call Ollama API
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-oss:20b',
prompt: prompt,
stream: false,
format: 'json'
}),
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status} ${response.statusText}. Make sure Ollama is running with 'ollama serve'.`);
}
const ollamaResult = await response.json();
let data;
try {
// Parse the JSON response from Ollama
data = JSON.parse(ollamaResult.response);
} catch (parseError) {
// Fallback to mock data if parsing fails
console.warn('Failed to parse Ollama response, using fallback data');
const fallbackData: Record<string, any> = {
"london": {
name: "London",
sys: { country: "GB" },
main: { temp: 15.2, feels_like: 14.8, humidity: 82 },
weather: [{ description: "light rain" }],
wind: { speed: 3.6 }
},
"new york": {
name: "New York",
sys: { country: "US" },
main: { temp: 22.5, feels_like: 24.1, humidity: 65 },
weather: [{ description: "clear sky" }],
wind: { speed: 2.1 }
},
"tokyo": {
name: "Tokyo",
sys: { country: "JP" },
main: { temp: 18.7, feels_like: 18.2, humidity: 78 },
weather: [{ description: "few clouds" }],
wind: { speed: 1.8 }
},
"paris": {
name: "Paris",
sys: { country: "FR" },
main: { temp: 12.8, feels_like: 11.9, humidity: 71 },
weather: [{ description: "overcast clouds" }],
wind: { speed: 4.2 }
},
"sydney": {
name: "Sydney",
sys: { country: "AU" },
main: { temp: 24.3, feels_like: 25.1, humidity: 73 },
weather: [{ description: "sunny" }],
wind: { speed: 2.8 }
}
};
data = fallbackData[city.toLowerCase().trim()] || fallbackData["london"];
}
// Format response
const tempUnit = units === "fahrenheit" ? "°F" : "°C";
const weatherText = `
Weather in ${data.name}, ${data.sys.country}:
- Temperature: ${data.main.temp}${tempUnit}
- Feels like: ${data.main.feels_like}${tempUnit}
- Conditions: ${data.weather[0].description}
- Humidity: ${data.main.humidity}%
- Wind Speed: ${data.wind.speed} m/s
*Generated by Ollama AI*
`.trim();
// Return MCP response
return {
content: [
{
type: "text",
text: weatherText
}
]
};
} catch (error) {
// Handle errors
throw new Error(
`Failed to get weather: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
if (name === "hello_world") {
const userName = args?.name as string;
if (!userName) {
throw new Error("Name parameter is required");
}
return {
content: [
{
type: "text",
text: `Hello, ${userName}! Welcome to your first MCP server! 🎉`,
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
}
);
}
/**
* Set up error handling
*/
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
/**
* Start the server
*/
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("My First MCP Server running on stdio");
}
}
/**
* Main entry point
*/
async function main() {
const server = new MyFirstMCPServer();
await server.start();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Testing the Weather Tool with Ollama:¶
Step 1: Install Ollama
# On macOS (using Homebrew)
brew install ollama
# Or download from the official website: https://ollama.ai/download
Step 2: Download a Model
# Pull the recommended model for this lab
ollama pull gpt-oss:20b
# List available models to verify the download completed successfully
ollama list
Step 3: Start Ollama Server
# Start the Ollama server (keep this running in a terminal)
ollama serve
# Check which models are currently running locally:
curl http://localhost:11434/api/tags | jq .
The server will run on http://localhost:11434 by default.
Step 4: Test the Weather Tool
-
Start the MCP Inspector (in a new terminal):
-
In the MCP Inspector interface:
- You should see both
get_weatherandhello_worldtools listed - Click on
get_weathertool - Enter a city name like “London”, “New York”, “Tokyo”, “Paris”, or “Sydney”
- Optionally set units to “fahrenheit” for Fahrenheit temperatures
- Click “Call Tool”
- You should see both
-
Test different scenarios:
- Valid cities: “London”, “New York”, “Tokyo”, “Paris”, “Sydney”
- Invalid cities: “InvalidCity123” (will use fallback data)
- Different units: Try both “celsius” and “fahrenheit”
- Empty city: Try with empty string (should show validation error)
-
Test error cases:
- Stop Ollama server and try calling the tool (should show API error)
- Try with invalid model name in the code (should show error)
Step 5: Verify Everything Works
-
You should see AI-generated weather information formatted like this:
Troubleshooting:
- If you get “Ollama API error”, make sure
ollama serveis running - If you see fallback data, Ollama might not be responding properly
- Check that the model is downloaded with
ollama list - Try a different model if gpt-oss:20b doesn’t work well
- Make sure you’re in the correct directory when running commands
Key Learning Points:¶
- Local AI integration using Ollama for generating content
- Fallback handling when AI services are unavailable
- JSON parsing from AI-generated responses
- Error handling for external service dependencies
- Input validation and data formatting
- API communication with local services
Tool 2: File Operations¶
Goal: Create a secure file reading tool that can handle various file types, validate paths, and return formatted content with metadata.
Complete File Operations Tool Implementation¶
Important: Do NOT copy the entire code block below. Instead, add the read_file tool to your existing src/index.ts file by following these specific steps:
-
Add the import at the top of your file (after existing imports):
-
Add the
read_filetool to your tools array in theListToolsRequestSchemahandler:{ name: "read_file", description: "Read contents of a text file with security validation", inputSchema: { type: "object", properties: { filepath: { type: "string", description: "Absolute path to the file" }, encoding: { type: "string", description: "File encoding", enum: ["utf8", "ascii", "base64"], default: "utf8" }, maxSize: { type: "number", description: "Maximum file size in bytes", minimum: 1, maximum: 10485760, default: 1048576 } }, required: ["filepath"] } } -
Add the
read_filehandler in theCallToolRequestSchemahandler (before the finalthrow new Error):if (name === "read_file") { try { const filepath = args.filepath as string; const encoding = (args.encoding as BufferEncoding) || "utf8"; const maxSize = (args.maxSize as number) || 1048576; // Security: Validate input if (!filepath || typeof filepath !== 'string' || filepath.trim().length === 0) { throw new Error("filepath must be a non-empty string"); } // Security: Resolve and validate path const resolvedPath = path.resolve(filepath); // Prevent directory traversal attacks if (!resolvedPath.startsWith(process.cwd())) { throw new Error("Access denied: file path outside allowed directory"); } // Check if file exists and is readable try { await fs.access(resolvedPath, fs.constants.R_OK); } catch { throw new Error(`File not found or not readable: ${filepath}`); } // Get file stats const stats = await fs.stat(resolvedPath); // Check if it's actually a file (not a directory) if (!stats.isFile()) { throw new Error(`Path is not a file: ${filepath}`); } // Check file size if (stats.size > maxSize) { throw new Error( `File too large: ${stats.size} bytes (max: ${maxSize})` ); } // Read file content const content = await fs.readFile(resolvedPath, encoding); // Format response with metadata const fileInfo = { path: resolvedPath, size: stats.size, modified: stats.mtime.toISOString(), encoding: encoding }; return { content: [ { type: "text", text: `File Information:\n${JSON.stringify(fileInfo, null, 2)}\n\nContent:\n${content}` } ] }; } catch (error) { throw new Error( `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }
The code block below is for reference only - it shows what your complete file should look like after adding the tool. Do not copy-paste the entire block:
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as fs from 'fs/promises';
import * as path from 'path';
/**
* Create an MCP server with core capabilities
*/
class MyFirstMCPServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: "my-first-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
/**
* Set up request handlers
*/
private setupHandlers(): void {
// Handler for listing available tools
this.server.setRequestHandler(
ListToolsRequestSchema,
async () => ({
tools: [
{
name: "get_weather",
description: "Get current weather information for a city using AI",
inputSchema: {
type: "object",
properties: {
city: {
type: "string",
description: "City name (e.g., 'London', 'New York')"
},
units: {
type: "string",
description: "Temperature units",
enum: ["celsius", "fahrenheit"],
default: "celsius"
}
},
required: ["city"]
}
},
{
name: "read_file",
description: "Read contents of a text file with security validation",
inputSchema: {
type: "object",
properties: {
filepath: {
type: "string",
description: "Absolute path to the file"
},
encoding: {
type: "string",
description: "File encoding",
enum: ["utf8", "ascii", "base64"],
default: "utf8"
},
maxSize: {
type: "number",
description: "Maximum file size in bytes",
minimum: 1,
maximum: 10485760,
default: 1048576
}
},
required: ["filepath"]
}
},
{
name: "hello_world",
description: "Returns a friendly greeting message",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "The name to greet",
},
},
required: ["name"],
},
},
],
})
);
// Handler for calling tools
this.server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
const { name, arguments: args } = request.params;
if (name === "get_weather") {
// ... existing weather tool code ...
}
if (name === "read_file") {
try {
const filepath = args.filepath as string;
const encoding = (args.encoding as BufferEncoding) || "utf8";
const maxSize = (args.maxSize as number) || 1048576;
// Security: Validate input
if (!filepath || typeof filepath !== 'string' || filepath.trim().length === 0) {
throw new Error("filepath must be a non-empty string");
}
// Security: Resolve and validate path
const resolvedPath = path.resolve(filepath);
// Prevent directory traversal attacks
if (!resolvedPath.startsWith(process.cwd())) {
throw new Error("Access denied: file path outside allowed directory");
}
// Check if file exists and is readable
try {
await fs.access(resolvedPath, fs.constants.R_OK);
} catch {
throw new Error(`File not found or not readable: ${filepath}`);
}
// Get file stats
const stats = await fs.stat(resolvedPath);
// Check if it's actually a file (not a directory)
if (!stats.isFile()) {
throw new Error(`Path is not a file: ${filepath}`);
}
// Check file size
if (stats.size > maxSize) {
throw new Error(
`File too large: ${stats.size} bytes (max: ${maxSize})`
);
}
// Read file content
const content = await fs.readFile(resolvedPath, encoding);
// Format response with metadata
const fileInfo = {
path: resolvedPath,
size: stats.size,
modified: stats.mtime.toISOString(),
encoding: encoding
};
return {
content: [
{
type: "text",
text: `File Information:\n${JSON.stringify(fileInfo, null, 2)}\n\nContent:\n${content}`
}
]
};
} catch (error) {
throw new Error(
`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
if (name === "hello_world") {
// ... existing hello world code ...
}
throw new Error(`Unknown tool: ${name}`);
}
);
}
// ... rest of the class remains the same ...
}
Testing the File Operations Tool¶
Step 1: Create Test Files
# Create a test directory and files
mkdir -p test-files
echo "Hello, this is a test file!" > test-files/hello.txt
echo '{"name": "test", "value": 123}' > test-files/data.json
echo "Line 1\nLine 2\nLine 3" > test-files/lines.txt
Step 2: Start the MCP Inspector
Step 3: Test File Reading
-
Test with a simple text file:
- Tool:
read_file - filepath:
/absolute/path/to/test-files/hello.txt(use the full absolute path) - Click “Call Tool”
- Tool:
-
Test with JSON file:
- Tool:
read_file - filepath:
/absolute/path/to/test-files/data.json - Click “Call Tool”
- Tool:
-
Test with different encoding:
- Tool:
read_file - filepath:
/absolute/path/to/test-files/hello.txt - encoding:
base64 - Click “Call Tool”
- Tool:
-
Test file size limit:
- Create a large file:
dd if=/dev/zero of=test-files/large.txt bs=1M count=2 - Try reading it with default maxSize (1MB)
- Try with maxSize:
2097152(2MB)
- Create a large file:
Step 4: Test Error Cases
-
Non-existent file:
- filepath:
/absolute/path/to/test-files/nonexistent.txt
- filepath:
-
Directory instead of file:
- filepath:
/absolute/path/to/test-files(the directory itself)
- filepath:
-
Empty filepath:
- filepath:
""
- filepath:
-
Path traversal attempt:
- filepath:
/absolute/path/../../../etc/passwd
- filepath:
Step 5: Verify Output
-
You should see output like:
Troubleshooting:¶
- “File not found”: Make sure you’re using the absolute path
- “Access denied”: The file path is outside your project directory
- “Path is not a file”: You tried to read a directory
- “File too large”: Increase the maxSize parameter
Key Learning Points:¶
- Path security and preventing directory traversal attacks
- File system operations with Node.js fs/promises
- Input validation beyond JSON Schema
- File metadata extraction and formatting
- Error handling for various file system scenarios
- Resource limits to prevent abuse
Tool 3: Database Query¶
Goal: Create a secure database query tool that can execute SELECT statements on a SQLite database with proper validation and safety measures.
Complete Database Query Tool Implementation¶
-
First, install the SQLite dependency:
Important: Do NOT copy the entire code block below. Instead, add the query_database tool to your existing src/index.ts file by following these specific steps:
-
Add the imports at the top of your file (after existing imports):
-
Add the
query_databasetool to your tools array in theListToolsRequestSchemahandler:{ name: "query_database", description: "Execute SELECT queries on a SQLite database", inputSchema: { type: "object", properties: { query: { type: "string", description: "SQL SELECT query to execute" }, parameters: { type: "array", description: "Query parameters for prepared statement", items: { type: ["string", "number", "boolean", "null"] }, default: [] }, limit: { type: "number", description: "Maximum number of rows to return", minimum: 1, maximum: 1000, default: 100 } }, required: ["query"] } } -
Add the
query_databasehandler in theCallToolRequestSchemahandler (before the finalthrow new Error):if (name === "query_database") { try { const query = args.query as string; const parameters = (args.parameters as any[]) || []; const limit = (args.limit as number) || 100; // Security: Validate input if (!query || typeof query !== 'string' || query.trim().length === 0) { throw new Error("query must be a non-empty string"); } // Security: Only allow SELECT queries const trimmedQuery = query.trim().toUpperCase(); if (!trimmedQuery.startsWith('SELECT')) { throw new Error("Only SELECT queries are allowed for security"); } // Check if database file exists const dbPath = './data.db'; try { await fs.access(dbPath, fs.constants.R_OK); } catch { throw new Error("Database file 'data.db' not found in project root"); } // Open database in read-only mode const db = new Database(dbPath, { readonly: true }); try { // Prepare statement const stmt = db.prepare(query + ' LIMIT ?'); // Execute query const rows = stmt.all(...parameters, limit); // Format results const resultText = rows.length > 0 ? JSON.stringify(rows, null, 2) : "No results found"; // Get query info const info = stmt.columns(); const columnNames = info.map(col => col.name); return { content: [ { type: "text", text: `Query executed successfully.\nDatabase: ${dbPath}\nColumns: ${columnNames.join(', ')}\nRows returned: ${rows.length}\n\nResults:\n${resultText}` } ] }; } finally { db.close(); } } catch (error) { throw new Error( `Database query failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }
-
The code block below is for reference only - it shows what your complete file should look like after adding the tool. Do not copy-paste the entire block:
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import * as fs from 'fs/promises'; import * as path from 'path'; import Database from 'better-sqlite3'; // ... existing imports and class definition ... /** * Set up request handlers */ private setupHandlers(): void { // Handler for listing available tools this.server.setRequestHandler( ListToolsRequestSchema, async () => ({ tools: [ // ... existing tools ... { name: "query_database", description: "Execute SELECT queries on a SQLite database", inputSchema: { type: "object", properties: { query: { type: "string", description: "SQL SELECT query to execute" }, parameters: { type: "array", description: "Query parameters for prepared statement", items: { type: ["string", "number", "boolean", "null"] }, default: [] }, limit: { type: "number", description: "Maximum number of rows to return", minimum: 1, maximum: 1000, default: 100 } }, required: ["query"] } }, // ... existing tools ... ], }) ); // Handler for calling tools this.server.setRequestHandler( CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // ... existing tool handlers ... if (name === "query_database") { try { const query = args.query as string; const parameters = (args.parameters as any[]) || []; const limit = (args.limit as number) || 100; // Security: Validate input if (!query || typeof query !== 'string' || query.trim().length === 0) { throw new Error("query must be a non-empty string"); } // Security: Only allow SELECT queries const trimmedQuery = query.trim().toUpperCase(); if (!trimmedQuery.startsWith('SELECT')) { throw new Error("Only SELECT queries are allowed for security"); } // Check if database file exists const dbPath = './data.db'; try { await fs.access(dbPath, fs.constants.R_OK); } catch { throw new Error("Database file 'data.db' not found in project root"); } // Open database in read-only mode const db = new Database(dbPath, { readonly: true }); try { // Prepare statement const stmt = db.prepare(query + ' LIMIT ?'); // Execute query const rows = stmt.all(...parameters, limit); // Format results const resultText = rows.length > 0 ? JSON.stringify(rows, null, 2) : "No results found"; // Get query info const info = stmt.columns(); const columnNames = info.map(col => col.name); return { content: [ { type: "text", text: `Query executed successfully.\nDatabase: ${dbPath}\nColumns: ${columnNames.join(', ')}\nRows returned: ${rows.length}\n\nResults:\n${resultText}` } ] }; } finally { db.close(); } } catch (error) { throw new Error( `Database query failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // ... existing tool handlers ... } ); } // ... rest of the class remains the same ...
Testing the Database Query Tool¶
Step 1: Install SQLite
# Install sqlite3 command-line tool (if not already installed)
# On macOS:
brew install sqlite3
# On Linux (Ubuntu/Debian):
sudo apt-get update && sudo apt-get install sqlite3
# On Linux (CentOS/RHEL/Fedora):
sudo yum install sqlite3 # or sudo dnf install sqlite3
# On Windows (using Chocolatey):
choco install sqlite
# On Windows (manual download):
# Download from: https://www.sqlite.org/download.html
# Extract sqlite3.exe to a folder in your PATH
# Verify installation:
sqlite3 --version
Step 2: Create a Sample Database
Navigate to your MCP server directory
Run the database creation command
sqlite3 data.db << 'EOF'
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL,
category TEXT,
in_stock BOOLEAN DEFAULT 1
);
INSERT INTO users (name, email, age) VALUES
('Alice Johnson', 'alice@example.com', 28),
('Bob Smith', 'bob@example.com', 34),
('Charlie Brown', 'charlie@example.com', 22);
INSERT INTO products (name, price, category, in_stock) VALUES
('Laptop', 999.99, 'Electronics', 1),
('Book', 19.99, 'Education', 1),
('Coffee Mug', 12.50, 'Kitchen', 0);
.quit
EOF
Verify the database was created
-
You should see output like:
What this does:
- Creates a SQLite database file called
data.dbin your project directory - Creates two tables:
usersandproducts - Inserts sample data into both tables
- This gives you test data to query with your
query_databasetool
Step 2: Start the MCP Inspector
Step 3: Test Database Queries
-
Simple SELECT query:
- Tool:
query_database - query:
SELECT * FROM users - Click “Call Tool”
- Tool:
-
Query with WHERE clause:
- Tool:
query_database - query:
SELECT name, email FROM users WHERE age > 25 - Click “Call Tool”
- Tool:
-
Query with parameters:
- Tool:
query_database - query:
SELECT * FROM products WHERE category = ? - parameters:
["Electronics"] - Click “Call Tool”
- Tool:
-
Query with LIMIT:
- Tool:
query_database - query:
SELECT * FROM users - limit:
2 - Click “Call Tool”
- Tool:
-
JOIN query:
- Tool:
query_database - query:
SELECT u.name, p.name as product FROM users u CROSS JOIN products p LIMIT 5 - Click “Call Tool”
- Tool:
Step 4: Test Error Cases
-
Non-SELECT query:
- query:
DELETE FROM users WHERE id = 1
- query:
-
Invalid SQL syntax:
- query:
SELECT * FROM nonexistent_table
- query:
-
Missing database file:
- Rename
data.dbtodata.db.backupand try a query
- Rename
-
Empty query:
- query:
""
- query:
Step 5: Verify Output
-
You should see output like:
Troubleshooting:
- “Database file not found”: Make sure
data.dbexists in your project root - “Only SELECT queries are allowed”: The tool only allows SELECT statements for security
- “no such table”: Check your table names in the database
- “sqlite3: command not found”: Install sqlite3 CLI tool
Key Learning Points:¶
- SQL injection prevention using prepared statements
- Database security with read-only access and query restrictions
- SQLite operations with better-sqlite3
- Query parameterization for safe dynamic queries
- Result formatting and metadata extraction
- Resource management with proper database connection handling
Returning Rich Content¶
MCP supports multiple content types in tool responses, allowing you to return not just text but also images, resources, and combinations of different content types. This enables richer, more interactive responses that can include visual data, file references, and structured information.
1. Text Content¶
Text content is the most common and basic type of response. Use it for any string-based information like analysis results, status messages, or formatted data.
When to use - Most tool responses will use text content. It’s perfect for:
- Status messages and confirmations
- Formatted data output (JSON, tables, lists)
- Error messages and explanations
- Analysis results and summaries
2. Image Content¶
Image content allows you to return visual data directly in the response. The image data must be base64-encoded and include the appropriate MIME type.
When to use - Ideal for tools that generate or process visual content:
- Charts and graphs from data analysis
- Screenshots or visual captures
- Generated diagrams or illustrations
- Image processing results
Important: Always specify the correct MIME type (image/png, image/jpeg, image/svg+xml, etc.) and ensure the base64 data is properly encoded.
3. Resource Content¶
Resource content references external resources rather than including their data directly. This is useful for large files or when you want to provide access to resources without embedding them.
return {
content: [
{
type: "resource",
resource: {
uri: "file:///path/to/file.txt",
mimeType: "text/plain",
text: "File contents..."
}
}
]
};
When to use - Best for:
- Large files that would make responses too bulky
- References to external files or URLs
- When the client should handle the resource directly
- Providing access to generated files
Note: The text field is optional - you can omit it if the resource content is too large or if you just want to provide a reference.
4. Multiple Content Items¶
Multiple content items allow you to combine different types of content in a single response. This creates rich, multi-part responses that can include text explanations alongside visual data.
return {
content: [
{
type: "text",
text: "Analysis complete:"
},
{
type: "text",
text: "Details:\n- Item 1\n- Item 2"
},
{
type: "image",
data: chartImage,
mimeType: "image/png"
}
]
};
When to use - Perfect for comprehensive responses that need multiple components:
- Analysis reports with both text summaries and visual charts
- File processing results with metadata and content preview
- Multi-step operations with status updates and final results
- Complex data with both tabular and graphical representations
Tip: Order your content logically - start with text explanations, then show supporting images or resources.
Error Handling Patterns¶
Error handling is crucial for robust MCP tools. Different situations require different approaches to handle failures gracefully while providing useful feedback to users. Here are three essential patterns for handling errors effectively.
Pattern 1: Input Validation¶
Input validation ensures that tool arguments meet your requirements before processing begins. This prevents runtime errors and provides clear feedback when users provide invalid data.
function validateInput(args: any): void {
if (!args.filepath || typeof args.filepath !== 'string') {
throw new Error("filepath must be a non-empty string");
}
if (args.maxSize && (args.maxSize < 1 || args.maxSize > 10485760)) {
throw new Error("maxSize must be between 1 and 10485760 bytes");
}
}
When to use - Always validate inputs before processing, even when using JSON Schema validation. This pattern is essential for:
- Type checking beyond JSON Schema capabilities
- Business logic validation (file size limits, path security)
- Preventing runtime errors from malformed data
- Providing specific, actionable error messages
Why it matters: Early validation fails fast and gives users clear guidance on how to fix their input.
Pattern 2: Graceful Degradation¶
Graceful degradation provides partial functionality when full operation isn’t possible. Instead of failing completely, the tool returns useful information or falls back to alternative approaches.
try {
const data = await fetchFromAPI(url);
return formatSuccess(data);
} catch (error) {
// Log error but return partial results if possible
console.error("API call failed:", error);
return {
content: [
{
type: "text",
text: "⚠️ Could not fetch live data. Using cached results..."
}
]
};
}
When to use - For external dependencies that might be unreliable:
- API calls that could timeout or fail
- Network-dependent operations
- Services with occasional downtime
- When partial results are better than no results
Why it matters: Users get some value even when systems are partially broken, improving overall reliability and user experience.
Pattern 3: Detailed Error Context¶
Detailed error context provides comprehensive information for debugging while keeping user-facing messages clean. Log full details internally but expose only safe, helpful information to users.
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const errorContext = {
tool: name,
arguments: args,
timestamp: new Date().toISOString(),
error: errorMessage
};
console.error("[Tool Error]", JSON.stringify(errorContext));
throw new Error(
`Tool '${name}' failed: ${errorMessage}. Check server logs for details.`
);
}
When to use - For complex operations where debugging might be needed:
- Multi-step processes with potential failure points
- Operations involving external systems
- When you need to track error patterns over time
- Production environments where detailed logging is crucial
Why it matters: Developers can diagnose issues effectively while users get clear, non-technical error messages.
Best Practices for Error Handling:¶
- Fail Fast: Validate inputs early and stop processing on critical errors
- Log Internally: Use
console.error()for detailed logging (goes to stderr, not stdout) - User-Friendly Messages: Keep error messages clear and actionable
- Don’t Leak Sensitive Data: Never expose file paths, credentials, or internal details
- Consistent Format: Use similar error message patterns across tools
- Recovery Options: When possible, suggest how users can resolve the issue
Async Operations and Performance¶
MCP tools often need to handle asynchronous operations and optimize performance. Long-running tasks require special handling to prevent timeouts and provide feedback, while expensive operations benefit from caching to improve response times and reduce resource usage.
Long-Running Operations¶
Long-running operations need monitoring and progress feedback to prevent timeouts and keep users informed. Use logging and timing to track operation progress and provide completion status.
if (name === "analyze_large_file") {
const filepath = args.filepath as string;
// For very long operations, consider streaming or progress updates
console.error(`[INFO] Starting analysis of ${filepath}...`);
try {
const startTime = Date.now();
// Perform analysis
const result = await performLongAnalysis(filepath);
const duration = Date.now() - startTime;
console.error(`[INFO] Analysis completed in ${duration}ms`);
return {
content: [
{
type: "text",
text: `Analysis Results (completed in ${duration}ms):\n\n${result}`
}
]
};
} catch (error) {
console.error(`[ERROR] Analysis failed after ${Date.now() - startTime}ms`);
throw error;
}
}
When to use - For operations that take more than a few seconds:
- Large file processing or analysis
- Complex computations
- External API calls with potential delays
- Batch operations on multiple items
Why it matters: Prevents timeouts, provides user feedback, enables monitoring and debugging of slow operations.
Caching Results¶
Caching results stores expensive operation results to avoid redundant computation. Use time-based expiration and proper cache keys for efficient reuse of results.
class CachedMCPServer {
private cache: Map<string, { data: any; timestamp: number }>;
private cacheTTL: number = 60000; // 1 minute
constructor() {
this.cache = new Map();
}
private getCacheKey(toolName: string, args: any): string {
return `${toolName}:${JSON.stringify(args)}`;
}
private getCached(key: string): any | null {
const cached = this.cache.get(key);
if (!cached) return null;
if (Date.now() - cached.timestamp > this.cacheTTL) {
this.cache.delete(key);
return null;
}
return cached.data;
}
private setCache(key: string, data: any): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
}
When to use - For expensive operations that return consistent results:
- API calls to external services
- Complex calculations or data processing
- Database queries with static data
- File analysis that doesn’t change frequently
Why it matters: Dramatically improves response times, reduces resource usage, and provides better user experience for repeated requests.
Best Practices for Async Operations and Performance:¶
- Monitor Execution Time: Log start/end times for operations over 1 second
- Set Reasonable Timeouts: Use appropriate timeouts for external calls (5-30 seconds)
- Cache Strategically: Cache expensive operations but consider data freshness
- Use Streaming: For very large responses, consider streaming or pagination
- Resource Cleanup: Always clean up connections, file handles, and memory
- Progress Feedback: For long operations, provide progress updates via logging
- Memory Management: Be mindful of memory usage in long-running processes
Tool Composition¶
Tool composition is the art of designing MCP tools that work seamlessly together, allowing LLMs to chain multiple tools to accomplish complex tasks. Well-composed tools create a powerful ecosystem where each tool handles a specific responsibility while enabling sophisticated workflows through intelligent combination.
Example: Multi-Step Analysis¶
Multi-step analysis demonstrates how simple, focused tools can be combined to perform complex data processing workflows. Each tool has a clear responsibility and can be used independently or as part of larger operations.
// Tool 1: List files
{
name: "list_files",
description: "List files in a directory",
inputSchema: { ... }
}
// Tool 2: Read file
{
name: "read_file",
description: "Read a specific file",
inputSchema: { ... }
}
// Tool 3: Analyze content
{
name: "analyze_text",
description: "Analyze text content",
inputSchema: { ... }
}
When to use - For workflows that require multiple processing steps:
- Data analysis pipelines
- File processing workflows
- Multi-stage computations
- Complex research tasks
Why it matters: Breaks down complex problems into manageable, reusable components that can be combined in flexible ways.
LLM Tool Chaining¶
LLM tool chaining allows AI models to automatically sequence tool calls based on intermediate results. The LLM analyzes outputs from one tool and determines which tool to call next, creating intelligent workflows without explicit programming.
The LLM can chain these tools:
- List files in directory - Discover available files
- Read interesting files - Access content based on filenames
- Analyze their content - Process and extract insights
When to use - When tasks naturally break down into sequential steps:
- Research and analysis workflows
- Data processing pipelines
- Content generation chains
- Problem-solving sequences
Why it matters: Enables complex, multi-step reasoning and problem-solving that would be difficult to implement in single tools.
Testing Composed Tools¶
Testing composed tools ensures that individual tools work correctly both in isolation and when chained together. Use comprehensive test suites that cover single-tool usage and multi-tool workflows.
import { describe, it, expect } from 'vitest';
describe('Weather Tool', () => {
it('should validate city name', async () => {
await expect(
callTool('get_weather', { city: '' })
).rejects.toThrow('City name cannot be empty');
});
it('should handle invalid city', async () => {
await expect(
callTool('get_weather', { city: 'InvalidCity12345' })
).rejects.toThrow('not found');
});
it('should return weather data', async () => {
const result = await callTool('get_weather', {
city: 'London',
units: 'celsius'
});
expect(result.content).toHaveLength(1);
expect(result.content[0].text).toContain('Temperature');
});
});
When to use - For validating tool behavior in different scenarios:
- Unit testing individual tools
- Integration testing tool chains
- Regression testing after changes
- Edge case validation
Why it matters: Ensures reliability and predictability when tools are used individually or in combination.
Best Practices for Tool Composition:¶
- Single Responsibility: Each tool should do one thing well
- Consistent Interfaces: Use similar parameter patterns across tools
- Clear Dependencies: Document which tools work well together
- Error Propagation: Handle failures gracefully in tool chains
- State Management: Avoid tools that require complex state between calls
- Flexible Outputs: Design tool outputs to be usable as inputs for other tools
- Documentation: Clearly explain how tools can be combined
- Version Compatibility: Ensure tool interfaces remain stable
Best Practices Checklist¶
✅ Schema Design
- Use descriptive names and descriptions
- Add examples in descriptions
- Set reasonable defaults
- Use enums for constrained values
- Add min/max for numbers
✅ Implementation
- Validate all inputs, even with schemas
- Handle errors gracefully
- Log to stderr, not stdout
- Use async/await properly
- Clean up resources (file handles, connections)
✅ Security
- Validate and sanitize file paths
- Use prepared statements for SQL
- Limit resource usage (file sizes, API calls)
- Don’t expose sensitive data in errors
- Implement rate limiting
✅ Performance
- Cache expensive operations
- Set reasonable timeouts
- Limit result sizes
- Use streaming for large data
- Monitor execution time
✅ User Experience
- Provide clear error messages
- Return structured data when possible
- Include relevant context in responses
- Handle edge cases gracefully
- Document expected behavior
Hands-On Exercises¶
Exercise 1: Text Processing Tool¶
Create a tool that:
- Counts words, characters, lines
- Finds specific patterns
- Calculates reading time
- Detects language
💡 Solution: Text Processing Tool
Tool Schema - Add this to your tools array:{
name: "process_text",
description: "Analyze and process text content with various metrics and operations",
inputSchema: {
type: "object",
properties: {
text: {
type: "string",
description: "The text content to process"
},
operations: {
type: "array",
description: "Operations to perform",
items: {
type: "string",
enum: ["count", "find_pattern", "reading_time", "detect_language"]
},
default: ["count"]
},
pattern: {
type: "string",
description: "Regex pattern for find_pattern operation"
}
},
required: ["text"]
}
}
if (name === "process_text") {
try {
const text = args.text as string;
const operations = (args.operations as string[]) || ["count"];
const pattern = args.pattern as string;
let results: string[] = [];
for (const op of operations) {
switch (op) {
case "count":
const lines = text.split('\n').length;
const words = text.split(/\s+/).filter(w => w.length > 0).length;
const chars = text.length;
results.push(`📊 Text Statistics:\n- Lines: ${lines}\n- Words: ${words}\n- Characters: ${chars}`);
break;
case "find_pattern":
if (!pattern) {
results.push("❌ Pattern required for find_pattern operation");
break;
}
try {
const regex = new RegExp(pattern, 'g');
const matches = text.match(regex);
results.push(`🔍 Pattern Matches (${pattern}):\nFound ${matches ? matches.length : 0} matches:\n${matches ? matches.slice(0, 10).join('\n') : 'None'}`);
} catch (e) {
results.push(`❌ Invalid regex pattern: ${pattern}`);
}
break;
case "reading_time":
// Average reading speed: 200 words per minute
const wordCount = text.split(/\s+/).filter(w => w.length > 0).length;
const readingTime = Math.ceil(wordCount / 200);
results.push(`⏱️ Reading Time: Approximately ${readingTime} minute${readingTime !== 1 ? 's' : ''} (${wordCount} words at 200 WPM)`);
break;
case "detect_language":
// Simple language detection based on common words
const englishWords = /\b(the|and|or|but|in|on|at|to|for|of|with|by)\b/gi;
const spanishWords = /\b(el|la|los|las|y|o|pero|en|sobre|a|para|de|con|por)\b/gi;
const frenchWords = /\b(le|la|les|et|ou|mais|dans|sur|à|pour|de|avec|par)\b/gi;
const englishMatches = (text.match(englishWords) || []).length;
const spanishMatches = (text.match(spanishWords) || []).length;
const frenchMatches = (text.match(frenchWords) || []).length;
const maxMatches = Math.max(englishMatches, spanishMatches, frenchMatches);
let detectedLang = "Unknown";
if (maxMatches > 0) {
if (englishMatches === maxMatches) detectedLang = "English";
else if (spanishMatches === maxMatches) detectedLang = "Spanish";
else if (frenchMatches === maxMatches) detectedLang = "French";
}
results.push(`🌍 Detected Language: ${detectedLang} (confidence: ${maxMatches} common words)`);
break;
}
}
return {
content: [
{
type: "text",
text: results.join('\n\n')
}
]
};
} catch (error) {
throw new Error(`Text processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Basic counting
{ text: "Hello world\nThis is a test", operations: ["count"] }
// Pattern matching
{ text: "The quick brown fox jumps over the lazy dog", operations: ["find_pattern"], pattern: "\\b\\w{4}\\b" }
// Multiple operations
{ text: "This is a longer piece of text to analyze for various metrics and patterns.", operations: ["count", "reading_time", "detect_language"] }
Exercise 2: JSON Validator Tool¶
Create a tool that:
- Validates JSON syntax
- Validates against JSON Schema
- Formats/pretty-prints JSON
- Compares two JSON objects
💡 Solution: JSON Validator Tool
Tool Schema - Add this to your tools array:{
name: "validate_json",
description: "Validate, format, and compare JSON data",
inputSchema: {
type: "object",
properties: {
json: {
type: "string",
description: "JSON string to validate or format"
},
operation: {
type: "string",
description: "Operation to perform",
enum: ["validate", "format", "schema_validate", "compare"],
default: "validate"
},
schema: {
type: "string",
description: "JSON Schema for schema validation (as JSON string)"
},
json2: {
type: "string",
description: "Second JSON string for comparison"
}
},
required: ["json", "operation"]
}
}
if (name === "validate_json") {
try {
const json = args.json as string;
const operation = args.operation as string;
const schema = args.schema as string;
const json2 = args.json2 as string;
let result = "";
switch (operation) {
case "validate":
try {
JSON.parse(json);
result = "✅ Valid JSON syntax";
} catch (e) {
result = `❌ Invalid JSON: ${e instanceof Error ? e.message : 'Unknown error'}`;
}
break;
case "format":
try {
const parsed = JSON.parse(json);
result = `📄 Formatted JSON:\n\`\`\`json\n${JSON.stringify(parsed, null, 2)}\n\`\`\``;
} catch (e) {
result = `❌ Cannot format invalid JSON: ${e instanceof Error ? e.message : 'Unknown error'}`;
}
break;
case "schema_validate":
if (!schema) {
result = "❌ Schema required for schema validation";
break;
}
try {
const ajv = new Ajv();
const parsedJson = JSON.parse(json);
const parsedSchema = JSON.parse(schema);
const validate = ajv.compile(parsedSchema);
const valid = validate(parsedJson);
if (valid) {
result = "✅ JSON validates against schema";
} else {
result = `❌ Schema validation failed:\n${JSON.stringify(validate.errors, null, 2)}`;
}
} catch (e) {
result = `❌ Schema validation error: ${e instanceof Error ? e.message : 'Unknown error'}`;
}
break;
case "compare":
if (!json2) {
result = "❌ Second JSON required for comparison";
break;
}
try {
const obj1 = JSON.parse(json);
const obj2 = JSON.parse(json2);
const differences: string[] = [];
// Simple comparison - check if objects are equal
if (JSON.stringify(obj1) === JSON.stringify(obj2)) {
result = "✅ JSON objects are identical";
} else {
// Find differences
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
const added = keys2.filter(k => !keys1.includes(k));
const removed = keys1.filter(k => !keys2.includes(k));
const modified = keys1.filter(k => keys2.includes(k) && JSON.stringify(obj1[k]) !== JSON.stringify(obj2[k]));
if (added.length > 0) differences.push(`Added keys: ${added.join(', ')}`);
if (removed.length > 0) differences.push(`Removed keys: ${removed.join(', ')}`);
if (modified.length > 0) differences.push(`Modified keys: ${modified.join(', ')}`);
result = `⚠️ JSON objects differ:\n${differences.join('\n')}`;
}
} catch (e) {
result = `❌ Comparison error: ${e instanceof Error ? e.message : 'Unknown error'}`;
}
break;
}
return {
content: [
{
type: "text",
text: result
}
]
};
} catch (error) {
throw new Error(`JSON validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Validate syntax
{ json: '{"name": "test", "value": 123}', operation: "validate" }
// Format JSON
{ json: '{"name":"test","value":123}', operation: "format" }
// Schema validation
{
json: '{"name": "John", "age": 30}',
operation: "schema_validate",
schema: '{"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "number"}}}'
}
// Compare JSON
{
json: '{"a": 1, "b": 2}',
json2: '{"a": 1, "c": 3}',
operation: "compare"
}
Exercise 3: Web Scraper Tool¶
Create a tool that:
- Fetches web page content
- Extracts specific elements
- Returns clean text
- Handles errors gracefully
💡 Solution: Web Scraper Tool
Tool Schema - Add this to your tools array:{
name: "scrape_web",
description: "Fetch and extract content from web pages",
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "URL to scrape",
format: "uri"
},
selector: {
type: "string",
description: "CSS selector to extract specific elements (optional)",
default: "body"
},
includeText: {
type: "boolean",
description: "Extract only text content (remove HTML)",
default: true
},
maxLength: {
type: "number",
description: "Maximum length of extracted content",
minimum: 100,
maximum: 10000,
default: 2000
},
timeout: {
type: "number",
description: "Request timeout in milliseconds",
minimum: 1000,
maximum: 30000,
default: 10000
}
},
required: ["url"]
}
}
if (name === "scrape_web") {
try {
const url = args.url as string;
const selector = (args.selector as string) || "body";
const includeText = (args.includeText !== false); // default true
const maxLength = (args.maxLength as number) || 2000;
const timeout = (args.timeout as number) || 10000;
// Validate URL
try {
new URL(url);
} catch {
throw new Error("Invalid URL format");
}
// Fetch the webpage
const response = await axios.get(url, {
timeout: timeout,
headers: {
'User-Agent': 'MCP-Web-Scraper/1.0 (Educational Tool)'
},
maxContentLength: 5 * 1024 * 1024, // 5MB limit
});
// Load HTML into cheerio
const $ = cheerio.load(response.data);
// Extract content based on selector
let extractedContent = "";
if (selector === "body") {
extractedContent = includeText ? $('body').text() : $('body').html() || "";
} else {
const elements = $(selector);
if (elements.length === 0) {
throw new Error(`No elements found matching selector: ${selector}`);
}
if (includeText) {
extractedContent = elements.map((_, el) => $(el).text()).get().join('\n\n');
} else {
extractedContent = elements.map((_, el) => $.html(el)).get().join('\n\n');
}
}
// Clean up the content
extractedContent = extractedContent
.replace(/\s+/g, ' ') // Replace multiple whitespace with single space
.replace(/\n\s*\n/g, '\n') // Remove empty lines
.trim();
// Truncate if too long
if (extractedContent.length > maxLength) {
extractedContent = extractedContent.substring(0, maxLength - 3) + "...";
}
// Prepare metadata
const metadata = {
url: url,
statusCode: response.status,
contentType: response.headers['content-type'],
contentLength: response.data.length,
extractedLength: extractedContent.length,
selector: selector,
elementsFound: selector === "body" ? 1 : $(selector).length
};
return {
content: [
{
type: "text",
text: `🌐 Web Scraping Results\n\n📊 Metadata:\n${Object.entries(metadata).map(([k, v]) => `- ${k}: ${v}`).join('\n')}\n\n📄 Extracted Content:\n${extractedContent}`
}
]
};
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.code === 'ENOTFOUND') {
throw new Error(`Could not resolve hostname: ${args.url}`);
} else if (error.code === 'ECONNREFUSED') {
throw new Error(`Connection refused: ${args.url}`);
} else if (error.response) {
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
} else if (error.code === 'ETIMEDOUT') {
throw new Error(`Request timeout after ${args.timeout || 10000}ms`);
} else {
throw new Error(`Network error: ${error.message}`);
}
} else {
throw new Error(`Web scraping failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
// Basic page scraping
{ url: "https://httpbin.org/html" }
// Extract specific elements
{ url: "https://httpbin.org/html", selector: "h1" }
// Get HTML instead of text
{ url: "https://httpbin.org/html", selector: "p", includeText: false }
// Test error handling
{ url: "https://nonexistent-domain-12345.com" }
{ url: "https://httpbin.org/status/404" }
Key Takeaways¶
✅ Well-designed tools have clear schemas with validation
✅ Always validate inputs, even with JSON Schema
✅ Return rich content types when appropriate
✅ Handle errors gracefully with helpful messages
✅ Consider performance and caching for expensive operations
✅ Design tools to be composable with others
✅ Test thoroughly with edge cases
✅ Security is paramount - validate and sanitize everything
Next Steps¶
In Lab 4, you’ll explore MCP Resources. You’ll learn:
- What resources are and how they differ from tools
- Implementing resource URIs and templates
- Supporting resource subscriptions for live updates
- Best practices for resource organization
- Combining tools and resources effectively
You’re becoming an MCP expert! Continue to Lab 4.
Lab 4: Implementing MCP Resources¶
Overview¶
In Lab 3, you mastered the art of creating sophisticated MCP tools that perform actions and return rich content. Now it’s time to explore MCP Resources - the passive counterpart to tools that provides contextual data for LLMs to read and reference.
Resources are the foundation for giving LLMs access to your knowledge bases, files, databases, and other data sources. Unlike tools that do things, resources are things - they represent the data itself that LLMs can access for context and reasoning.
Learning Objectives¶
By the end of this lab, you will:
- Understand the fundamental difference between tools and resources
- Design effective resource URI schemes for different data types
- Implement static and dynamic resources with proper metadata
- Create resource templates for parameterized access
- Build resource subscriptions for real-time updates
- Apply security best practices for resource access
- Combine resources with tools for comprehensive MCP servers
- Test resource implementations thoroughly
Prerequisites¶
- Completed Lab 3 - Implementing MCP Tools
- Understanding of URI / URL patterns and RESTful design
- Familiarity with file systems and data structures
- Basic knowledge of caching and performance optimization
Tools vs. Resources¶
The Fundamental Difference¶
Before diving into implementation, it’s crucial to understand when to use tools versus resources. They serve different purposes in the MCP ecosystem.
Tools: Active Operations¶
Tools perform actions and return results based on parameters.
// Tool: Active, parameterized, can have side effects
{
name: "search_database",
description: "Search database with custom query",
inputSchema: {
properties: {
query: { type: "string" },
limit: { type: "number", default: 100 }
}
}
}
When to use tools:
- Data changes frequently or needs computation
- Operations require parameters or user input
- Actions have side effects (create, update, delete)
- Results need processing or transformation
Resources: Passive Data¶
Resources expose existing data that can be read and referenced.
// Resource: Passive, addressable, read-only
{
uri: "db://users/123/profile",
name: "User Profile",
description: "User profile data for ID 123",
mimeType: "application/json"
}
When to use resources:
- Data is relatively static or changes predictably
- Direct access to structured data is needed
- Content should be cached or bookmarked
- Data serves as context for LLM reasoning
Decision Framework¶
| Scenario | Use Tool | Use Resource | Why |
|---|---|---|---|
| Current weather | ✅ Tool | ❌ Resource | Data changes constantly |
| API documentation | ❌ Tool | ✅ Resource | Static reference material |
| Database search | ✅ Tool | ❌ Resource | Requires query parameters |
| User profile | ❌ Tool | ✅ Resource | Direct data access needed |
| File contents | ❌ Tool | ✅ Resource | Static file data |
| Generate report | ✅ Tool | ❌ Resource | Computation required |
Resource Fundamentals¶
Resource Structure¶
Every MCP resource has a consistent structure with metadata that helps LLMs understand what they’re accessing:
interface Resource {
uri: string; // Unique identifier (like a URL)
name: string; // Human-readable title
description: string; // What the resource contains
mimeType?: string; // Content type (optional but recommended)
}
Resource Content¶
When a resource is read, it returns structured content:
interface ResourceContent {
contents: Array<{
uri: string;
mimeType?: string;
text?: string; // For text content
blob?: string; // For binary content (base64)
}>;
}
Key Points:
- Resources are read-only by convention
- Content can be text or binary (base64 encoded)
- Multiple content items can be returned for complex resources
MIME (Multipurpose Internet Mail Extensions) typeshelp clients handle content appropriately
Designing Resource URI Schemes¶
Effective URI design is crucial for resource organization and discoverability. A good URI scheme should be:
- Hierarchical: Reflects data organization
- Descriptive: Self-documenting structure
- Consistent: Follows patterns across similar resources
- Extensible: Allows for future additions
Common URI Patterns¶
File System Resources¶
Database Resources¶
API Documentation¶
api://petstore/v1/swagger.json
api://github/repos/microsoft/vscode/issues
api://weather/current/london
Configuration Resources¶
URI Design Best Practices¶
- Use descriptive paths:
users/activevsu/a - Include identifiers:
orders/123vscurrent-order - Support hierarchies:
docs/api/v1/endpoints - Use query parameters moderately: Prefer path segments
- Be consistent: Same patterns for similar resources
Implementing Static Resources¶
Static resources represent fixed data that doesn’t change or changes infrequently. They’re perfect for documentation, configuration files, and reference data.
Complete Static Resource Server¶
Here’s a complete MCP server that exposes static resources:
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as fs from 'fs/promises';
import * as path from 'path';
/**
* MCP Server exposing static file resources
*/
class StaticResourceServer {
private server: Server;
private resourceRoot: string;
constructor(resourceRoot: string = './resources') {
this.resourceRoot = path.resolve(resourceRoot);
this.server = new Server(
{
name: "static-resource-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
private setupHandlers(): void {
// List available resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources = await this.discoverResources();
return { resources };
});
// Read specific resource
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const content = await this.readResource(uri);
return { contents: [content] };
});
}
private async discoverResources(): Promise<any[]> {
const resources: any[] = [];
try {
const files = await this.walkDirectory(this.resourceRoot);
for (const file of files) {
const relativePath = path.relative(this.resourceRoot, file);
const uri = `file:///${relativePath.replace(/\\/g, '/')}`;
const mimeType = this.getMimeType(file);
resources.push({
uri,
name: path.basename(file),
description: `Static file: ${relativePath}`,
mimeType,
});
}
} catch (error) {
console.error('Error discovering resources:', error);
}
return resources;
}
private async readResource(uri: string): Promise<any> {
// Validate URI format
if (!uri.startsWith('file:///')) {
throw new Error(`Invalid URI format: ${uri}`);
}
const relativePath = uri.substring('file:///'.length);
const filePath = path.join(this.resourceRoot, relativePath);
// Security: Prevent directory traversal
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(this.resourceRoot)) {
throw new Error('Access denied: path outside resource root');
}
try {
const content = await fs.readFile(filePath, 'utf8');
const mimeType = this.getMimeType(filePath);
return {
uri,
mimeType,
text: content,
};
} catch (error) {
throw new Error(`Failed to read resource: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private async walkDirectory(dir: string): Promise<string[]> {
const files: string[] = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip hidden directories and node_modules
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
files.push(...await this.walkDirectory(fullPath));
}
} else if (entry.isFile()) {
// Only include certain file types
const ext = path.extname(entry.name).toLowerCase();
if (['.md', '.txt', '.json', '.yaml', '.yml', '.js', '.ts', '.css', '.html'].includes(ext)) {
files.push(fullPath);
}
}
}
} catch (error) {
console.error(`Error walking directory ${dir}:`, error);
}
return files;
}
private getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes: { [key: string]: string } = {
'.md': 'text/markdown',
'.txt': 'text/plain',
'.json': 'application/json',
'.yaml': 'application/yaml',
'.yml': 'application/yaml',
'.js': 'application/javascript',
'.ts': 'application/typescript',
'.css': 'text/css',
'.html': 'text/html',
};
return mimeTypes[ext] || 'text/plain';
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error(`Static Resource Server running on stdio (root: ${this.resourceRoot})`);
}
}
// Main entry point
async function main() {
const server = new StaticResourceServer();
await server.start();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Testing Static Resources¶
Step 1: Create Test Resources
First, let’s create some test files that our static resource server can expose. These files will simulate a typical project structure with documentation, configuration, and general project files.
# Create the directory structure
mkdir -p resources/docs resources/config
# Create a markdown documentation file
echo "# API Documentation\n\nThis is the API docs." > resources/docs/api.md
# Create a JSON configuration file
echo '{"version": "1.0.0", "env": "development"}' > resources/config/settings.json
# Create a plain text README file
echo "Welcome to our project!" > resources/README.txt
What this creates:
resources/docs/api.md- A markdown file with API documentationresources/config/settings.json- A JSON configuration file with version and environment inforesources/README.txt- A plain text file with project information
Step 2: Start the Server
Now start your MCP server using the MCP Inspector.
This will launch both your server and the testing interface:
What to expect:
- The MCP Inspector window should open in your browser
- Your server will start and connect via
stdiotransport - You should see connection confirmation in the terminal
- The inspector interface will show tabs for Resources, Tools, etc.
Step 3: Test Resource Discovery
In the MCP Inspector, navigate to the Resources tab to see what resources your server is exposing.
-
The MCP Inspector should show resources like:
file:///docs/api.md- Your API documentation filefile:///config/settings.json- Your configuration filefile:///README.txt- Your project README
What to verify:
- All three resources should be listed
- Each resource should have a descriptive name and description
- MIME types should be correctly detected (text/markdown, application/json, text/plain)
- URIs should follow the
file://scheme with proper paths
Step 4: Test Resource Reading
Click on each resource in the list to read its content and verify proper handling.
-
Click on
file:///docs/api.md:- Should display: “# API Documentation\n\nThis is the API docs.”
- MIME type should be: text/markdown
-
Click on
file:///config/settings.json:- Should display: {“version”: “1.0.0”, “env”: “development”}
- MIME type should be: application/json
-
Click on
file:///README.txt:- Should display: “Welcome to our project!”
- MIME type should be: text/plain
Error Testing:
- Try reading a non-existent resource like
file:///does-not-exist.txt - Should show an appropriate error message
- Verify the server handles invalid URIs gracefully
What to learn:
- Resources provide direct access to file content
- MIME types help clients handle different content types
- Error handling is important for robust resource servers
- The inspector provides a complete testing environment
Implementing Dynamic Resources¶
Dynamic resources generate content on-demand based on parameters or current state. They’re useful for live data, computed views, and parameterized access.
Resource Templates¶
Resource templates allow parameterized URIs using {parameter} syntax:
// Template definition
{
uriTemplate: "db://users/{userId}/profile",
name: "User Profile",
description: "Profile data for a specific user",
mimeType: "application/json"
}
// Generated URIs
"db://users/123/profile"
"db://users/456/profile"
Complete Dynamic Resource Server¶
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListResourceTemplatesRequestSchema,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/types.js";
/**
* MCP Server with dynamic resources and templates
*/
class DynamicResourceServer {
private server: Server;
// Mock data store
private users = [
{ id: 1, name: "Alice Johnson", email: "alice@example.com", role: "admin" },
{ id: 2, name: "Bob Smith", email: "bob@example.com", role: "user" },
{ id: 3, name: "Charlie Brown", email: "charlie@example.com", role: "user" },
];
private products = [
{ id: 1, name: "Laptop", price: 999.99, category: "Electronics" },
{ id: 2, name: "Book", price: 19.99, category: "Education" },
{ id: 3, name: "Coffee Mug", price: 12.50, category: "Kitchen" },
];
constructor() {
this.server = new Server(
{
name: "dynamic-resource-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
private setupHandlers(): void {
// List resource templates
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
const templates: ResourceTemplate[] = [
{
uriTemplate: "db://users/{userId}/profile",
name: "User Profile",
description: "Profile information for a specific user",
mimeType: "application/json",
},
{
uriTemplate: "db://products/{productId}/details",
name: "Product Details",
description: "Detailed information about a product",
mimeType: "application/json",
},
{
uriTemplate: "db://stats/{category}/summary",
name: "Category Statistics",
description: "Statistical summary for a product category",
mimeType: "application/json",
},
];
return { resourceTemplates: templates };
});
// List available resources (dynamic)
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources: any[] = [];
// Add user resources
for (const user of this.users) {
resources.push({
uri: `db://users/${user.id}/profile`,
name: `User: ${user.name}`,
description: `Profile for ${user.name}`,
mimeType: "application/json",
});
}
// Add product resources
for (const product of this.products) {
resources.push({
uri: `db://products/${product.id}/details`,
name: `Product: ${product.name}`,
description: `${product.category} - $${product.price}`,
mimeType: "application/json",
});
}
// Add category stats
const categories = [...new Set(this.products.map(p => p.category))];
for (const category of categories) {
resources.push({
uri: `db://stats/${category}/summary`,
name: `${category} Statistics`,
description: `Summary statistics for ${category}`,
mimeType: "application/json",
});
}
return { resources };
});
// Read specific resource
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const content = await this.generateResourceContent(uri);
return { contents: [content] };
});
}
private async generateResourceContent(uri: string): Promise<any> {
const parts = uri.split('/');
if (parts[0] === 'db:' && parts[1] === '') {
const type = parts[2];
const id = parts[3];
const action = parts[4];
switch (type) {
case 'users':
if (action === 'profile') {
const user = this.users.find(u => u.id === parseInt(id));
if (!user) throw new Error(`User ${id} not found`);
return {
uri,
mimeType: "application/json",
text: JSON.stringify({
user,
metadata: {
lastUpdated: new Date().toISOString(),
source: "dynamic-resource-server"
}
}, null, 2),
};
}
break;
case 'products':
if (action === 'details') {
const product = this.products.find(p => p.id === parseInt(id));
if (!product) throw new Error(`Product ${id} not found`);
return {
uri,
mimeType: "application/json",
text: JSON.stringify({
product,
metadata: {
lastUpdated: new Date().toISOString(),
inStock: Math.random() > 0.3 // Simulate stock status
}
}, null, 2),
};
}
break;
case 'stats':
if (action === 'summary') {
const category = id;
const categoryProducts = this.products.filter(p => p.category === category);
if (categoryProducts.length === 0) {
throw new Error(`Category '${category}' not found`);
}
const stats = {
category,
totalProducts: categoryProducts.length,
averagePrice: categoryProducts.reduce((sum, p) => sum + p.price, 0) / categoryProducts.length,
priceRange: {
min: Math.min(...categoryProducts.map(p => p.price)),
max: Math.max(...categoryProducts.map(p => p.price)),
},
generatedAt: new Date().toISOString(),
};
return {
uri,
mimeType: "application/json",
text: JSON.stringify(stats, null, 2),
};
}
break;
}
}
throw new Error(`Unknown resource URI: ${uri}`);
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Dynamic Resource Server running on stdio");
}
}
// Main entry point
async function main() {
const server = new DynamicResourceServer();
await server.start();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Testing Dynamic Resources¶
Step 1: Start the Server
Step 2: Test Resource Templates
- Check that templates are listed:
db://users/{userId}/profile, etc.
Step 3: Test Resource Discovery
- Should show individual resources for users, products, and categories
Step 4: Test Resource Reading
- Try
db://users/1/profile- should return user data - Try
db://products/2/details- should return product data - Try
db://stats/Electronics/summary- should return category stats - Test invalid URIs to verify error handling
Resource Subscriptions for Live Updates¶
Resource subscriptions enable real-time updates when resource content changes. This is essential for live data, monitoring dashboards, and collaborative environments.
Subscription Implementation¶
// Enable subscriptions in server capabilities
{
capabilities: {
resources: {
subscribe: true // Enable subscription support
},
},
}
Complete Subscription Server¶
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
SubscribeRequestSchema,
UnsubscribeRequestSchema,
ResourceUpdatedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js";
/**
* MCP Server with resource subscriptions for live updates
*/
class SubscriptionResourceServer {
private server: Server;
private subscribers: Map<string, Set<string>> = new Map(); // uri -> sessionIds
private updateInterval: NodeJS.Timeout | null = null;
// Simulated live data
private metrics = {
activeUsers: 42,
serverLoad: 0.65,
responseTime: 120,
errorRate: 0.02,
};
constructor() {
this.server = new Server(
{
name: "subscription-resource-server",
version: "1.0.0",
},
{
capabilities: {
resources: {
subscribe: true,
},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
this.startMetricsSimulation();
}
private setupHandlers(): void {
// List resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "metrics://server/active-users",
name: "Active Users",
description: "Current number of active users",
mimeType: "application/json",
},
{
uri: "metrics://server/load",
name: "Server Load",
description: "Current server load percentage",
mimeType: "application/json",
},
{
uri: "metrics://server/response-time",
name: "Response Time",
description: "Average response time in milliseconds",
mimeType: "application/json",
},
{
uri: "metrics://server/error-rate",
name: "Error Rate",
description: "Current error rate percentage",
mimeType: "application/json",
},
],
};
});
// Read resource
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const content = this.getMetricContent(uri);
return { contents: [content] };
});
// Subscribe to resource updates
this.server.setRequestHandler(SubscribeRequestSchema, async (request) => {
const uri = request.params.uri;
if (!this.subscribers.has(uri)) {
this.subscribers.set(uri, new Set());
}
// In a real implementation, you'd get the session ID from the request
// For this example, we'll use a mock session ID
const sessionId = "mock-session";
this.subscribers.get(uri)!.add(sessionId);
console.error(`Subscribed to ${uri} (session: ${sessionId})`);
return {};
});
// Unsubscribe from resource updates
this.server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
const uri = request.params.uri;
const sessionId = "mock-session"; // Would come from request in real implementation
if (this.subscribers.has(uri)) {
this.subscribers.get(uri)!.delete(sessionId);
if (this.subscribers.get(uri)!.size === 0) {
this.subscribers.delete(uri);
}
}
console.error(`Unsubscribed from ${uri} (session: ${sessionId})`);
return {};
});
}
private getMetricContent(uri: string): any {
const metricName = uri.split('/').pop();
if (!metricName || !(metricName in this.metrics)) {
throw new Error(`Unknown metric: ${metricName}`);
}
const value = (this.metrics as any)[metricName];
return {
uri,
mimeType: "application/json",
text: JSON.stringify({
metric: metricName,
value,
timestamp: new Date().toISOString(),
unit: this.getMetricUnit(metricName),
}, null, 2),
};
}
private getMetricUnit(metricName: string): string {
const units: { [key: string]: string } = {
activeUsers: "users",
serverLoad: "percentage",
responseTime: "milliseconds",
errorRate: "percentage",
};
return units[metricName] || "unit";
}
private startMetricsSimulation(): void {
// Simulate changing metrics every 5 seconds
this.updateInterval = setInterval(() => {
// Randomly update metrics
this.metrics.activeUsers += Math.floor(Math.random() * 10) - 5;
this.metrics.activeUsers = Math.max(0, this.metrics.activeUsers);
this.metrics.serverLoad = Math.random() * 0.5 + 0.3; // 0.3 to 0.8
this.metrics.responseTime = 100 + Math.random() * 100; // 100-200ms
this.metrics.errorRate = Math.random() * 0.05; // 0-5%
// Notify subscribers of updates
this.notifySubscribers();
}, 5000);
}
private notifySubscribers(): void {
for (const [uri, sessionIds] of this.subscribers) {
if (sessionIds.size > 0) {
// Send notification to all subscribers of this resource
this.server.notification(ResourceUpdatedNotificationSchema, {
uri,
});
console.error(`Notified ${sessionIds.size} subscribers of ${uri} update`);
}
}
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
await this.server.close();
process.exit(0);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Subscription Resource Server running on stdio");
}
}
// Main entry point
async function main() {
const server = new SubscriptionResourceServer();
await server.start();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Testing Subscriptions¶
Step 1: Start the Server
Step 2: Test Basic Resource Reading
- Read metrics like
metrics://server/active-users - Verify they return current values
Step 3: Test Subscriptions
-
Subscribe to a metric resource:
- In the MCP Inspector Resources tab, find a metric resource (e.g.,
metrics://server/active-users) - Click the “Subscribe” button next to the resource
- You should see a confirmation that subscription was successful
- In the MCP Inspector Resources tab, find a metric resource (e.g.,
-
Watch the MCP Inspector for update notifications every 5 seconds:
- Keep the Resources tab open
- Look for notification messages in the inspector interface
- The metric values should update automatically every 5 seconds
- You can also check the console/logs for subscription update messages
-
Unsubscribe and verify notifications stop:
- Click the “Unsubscribe” button for the same resource
- Confirm that update notifications cease
- The metric values should stop updating in the interface
Security and Access Control¶
Resource security is critical, especially when exposing sensitive data or system information.
Access Control Patterns¶
1. Path-Based Authorization¶
private checkResourceAccess(uri: string, userId?: string): boolean {
// Allow access to own profile
if (uri.startsWith(`users/${userId}/`)) {
return true;
}
// Restrict admin resources
if (uri.startsWith('admin/') && !this.isAdmin(userId)) {
return false;
}
// Public resources
if (uri.startsWith('public/')) {
return true;
}
return false;
}
How it works: This method checks if a user has permission to access a resource based on the URI path. It grants access to user-specific resources (like their own profile), restricts admin-only resources unless the user has admin privileges, allows public resources for everyone, and denies access by default for any other paths.
2. Content Filtering¶
private filterSensitiveContent(content: any, userRole: string): any {
if (userRole !== 'admin') {
// Remove sensitive fields for non-admin users
const { password, ssn, ...filtered } = content;
return filtered;
}
return content;
}
How it works: This function removes sensitive information from resource content based on user roles. For non-admin users, it uses object destructuring to exclude sensitive fields like passwords and social security numbers, returning a filtered version of the data. Admin users see the complete, unfiltered content.
3. Rate Limiting¶
private rateLimiter = new Map<string, { count: number; resetTime: number }>();
private checkRateLimit(identifier: string, maxRequests: number = 100): boolean {
const now = Date.now();
const windowMs = 60000; // 1 minute
const record = this.rateLimiter.get(identifier);
if (!record || now > record.resetTime) {
this.rateLimiter.set(identifier, { count: 1, resetTime: now + windowMs });
return true;
}
if (record.count >= maxRequests) {
return false;
}
record.count++;
return true;
}
How it works: This implements a sliding window rate limiter using a Map to track request counts per identifier (like user ID or IP address). It allows up to maxRequests (default 100) within a 1-minute window. When the limit is exceeded, it returns false to block the request. The window resets automatically after the time period expires.
Combining Resources with Tools¶
The most powerful MCP servers combine resources and tools to provide a comprehensive functionality!
Complete Hybrid Server¶
The following example demonstrates a server that combines both resources and tools, showing how they work together to provide comprehensive functionality.
The server maintains a document store that can be both read as resources and modified through tools.
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
/**
* MCP Server combining resources and tools
*/
class HybridServer {
private server: Server;
private documents: Map<string, any> = new Map();
constructor() {
this.server = new Server(
{
name: "hybrid-server",
version: "1.0.0",
},
{
capabilities: {
resources: {}, // Enable resource capabilities
tools: {}, // Enable tool capabilities
},
}
);
// Initialize some sample documents
this.documents.set("doc1", {
id: "doc1",
title: "Getting Started Guide",
content: "This is a comprehensive guide to getting started...",
tags: ["tutorial", "beginner"],
created: new Date().toISOString(),
});
this.setupHandlers();
this.setupErrorHandling();
}
Key setup points:
- The server declares both
resources: {}andtools: {}capabilities - A Map is used to store documents in memory
- Sample data is initialized to demonstrate functionality
private setupHandlers(): void {
// Resource handlers - provide read-only access to documents
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources = Array.from(this.documents.entries()).map(([id, doc]) => ({
uri: `docs://documents/${id}`,
name: doc.title,
description: `Document: ${doc.title}`,
mimeType: "application/json",
}));
return { resources };
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const content = this.readDocument(uri);
return { contents: [content] };
});
// Tool handlers - provide write operations for documents
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_document",
description: "Create a new document",
inputSchema: {
type: "object",
properties: {
title: { type: "string" },
content: { type: "string" },
tags: {
type: "array",
items: { type: "string" },
default: [],
},
},
required: ["title", "content"],
},
},
{
name: "search_documents",
description: "Search documents by content or tags",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
tag: { type: "string" },
limit: { type: "number", default: 10 },
},
},
},
{
name: "update_document",
description: "Update an existing document",
inputSchema: {
type: "object",
properties: {
id: { type: "string" },
title: { type: "string" },
content: { type: "string" },
tags: { type: "array", items: { type: "string" } },
},
required: ["id"],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "create_document":
return this.createDocument(args);
case "search_documents":
return this.searchDocuments(args);
case "update_document":
return this.updateDocument(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
}
Resource vs Tool handlers:
- Resources expose existing documents for reading (
docs://documents/{id}) - Tools provide operations to create, search, and update documents
- Resources are passive (read-only), tools are active (perform actions)
private readDocument(uri: string): any {
const match = uri.match(/^docs:\/\/documents\/(.+)$/);
if (!match) throw new Error(`Invalid document URI: ${uri}`);
const id = match[1];
const doc = this.documents.get(id);
if (!doc) throw new Error(`Document not found: ${id}`);
return {
uri,
mimeType: "application/json",
text: JSON.stringify(doc, null, 2),
};
}
Resource reading: Parses the URI to extract the document ID, retrieves the document from the Map, and returns it as JSON content.
private createDocument(args: any): any {
const id = `doc${Date.now()}`;
const doc = {
id,
title: args.title,
content: args.content,
tags: args.tags || [],
created: new Date().toISOString(),
modified: new Date().toISOString(),
};
this.documents.set(id, doc);
return {
content: [
{
type: "text",
text: `Document created successfully!\n\nID: ${id}\nTitle: ${doc.title}\nURI: docs://documents/${id}`,
},
],
};
}
Tool implementation: Creates a new document with a timestamp-based ID, stores it in the Map, and returns success information including the new document’s URI.
private searchDocuments(args: any): any {
let results = Array.from(this.documents.values());
if (args.query) {
const query = args.query.toLowerCase();
results = results.filter(doc =>
doc.title.toLowerCase().includes(query) ||
doc.content.toLowerCase().includes(query)
);
}
if (args.tag) {
results = results.filter(doc => doc.tags.includes(args.tag));
}
const limit = args.limit || 10;
results = results.slice(0, limit);
const formatted = results.map(doc => ({
id: doc.id,
title: doc.title,
tags: doc.tags,
uri: `docs://documents/${doc.id}`,
}));
return {
content: [
{
type: "text",
text: `Found ${results.length} documents:\n\n${formatted.map(doc =>
`📄 ${doc.title} (${doc.tags.join(', ')})\n URI: ${doc.uri}`
).join('\n\n')}`,
},
],
};
}
Search tool: Filters documents by query string or tag, limits results, and returns formatted text output with document URIs for easy access.
private updateDocument(args: any): any {
const doc = this.documents.get(args.id);
if (!doc) throw new Error(`Document not found: ${args.id}`);
if (args.title) doc.title = args.title;
if (args.content) doc.content = args.content;
if (args.tags) doc.tags = args.tags;
doc.modified = new Date().toISOString();
return {
content: [
{
type: "text",
text: `Document updated successfully!\n\nID: ${doc.id}\nTitle: ${doc.title}\nModified: ${doc.modified}`,
},
],
};
}
Update tool: Modifies existing document fields and updates the modification timestamp, providing feedback about the changes made.
How resources and tools complement each other:
- Resources provide passive access: LLMs can read documents at
docs://documents/{id} - Tools enable active operations: LLMs can create, search, and update documents
- Integration: Tools can reference resources in their responses (e.g., “Read the new document at docs://documents/123”)
- Workflow: Create documents with tools, then read them as resources for context
Hands-On Exercises¶
Exercise 1: File System Resource Server¶
Create a tool that:
- Exposes a directory as MCP resources
- Supports different file types (text, JSON, Markdown)
- Implements proper security (no directory traversal)
- Includes file metadata (size, modified date)
💡 Solution: File System Resource Server
Tool Schema - Add this to your tools array:{
name: "create_file_resource_server",
description: "Create an MCP server that exposes a directory as resources",
inputSchema: {
type: "object",
properties: {
directory: {
type: "string",
description: "Directory path to expose as resources",
default: "./files"
},
allowedExtensions: {
type: "array",
description: "File extensions to include",
items: { type: "string" },
default: [".md", ".txt", ".json", ".yaml", ".yml"]
},
maxFileSize: {
type: "number",
description: "Maximum file size in bytes",
default: 1048576
}
},
required: ["directory"]
}
}
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as fs from 'fs/promises';
import * as path from 'path';
class FileSystemResourceServer {
private server: Server;
private rootDir: string;
private allowedExtensions: string[];
private maxFileSize: number;
constructor(rootDir: string = './files', allowedExtensions: string[] = ['.md', '.txt', '.json', '.yaml', '.yml'], maxFileSize: number = 1048576) {
this.rootDir = path.resolve(rootDir);
this.allowedExtensions = allowedExtensions;
this.maxFileSize = maxFileSize;
this.server = new Server(
{
name: "filesystem-resource-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources = await this.discoverFiles();
return { resources };
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const content = await this.readFile(uri);
return { contents: [content] };
});
}
private async discoverFiles(): Promise<any[]> {
const resources: any[] = [];
try {
const files = await this.walkDirectory(this.rootDir);
for (const file of files) {
const relativePath = path.relative(this.rootDir, file);
const uri = `file:///${relativePath.replace(/\\/g, '/')}`;
const stats = await fs.stat(file);
const mimeType = this.getMimeType(file);
resources.push({
uri,
name: path.basename(file),
description: `File: ${relativePath} (${this.formatFileSize(stats.size)})`,
mimeType,
});
}
} catch (error) {
console.error('Error discovering files:', error);
}
return resources;
}
private async readFile(uri: string): Promise<any> {
if (!uri.startsWith('file:///')) {
throw new Error(`Invalid URI format: ${uri}`);
}
const relativePath = uri.substring('file:///'.length);
const filePath = path.join(this.rootDir, relativePath);
// Security: Prevent directory traversal
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(this.rootDir)) {
throw new Error('Access denied: path outside allowed directory');
}
// Check if file extension is allowed
const ext = path.extname(filePath).toLowerCase();
if (!this.allowedExtensions.includes(ext)) {
throw new Error(`File type not allowed: ${ext}`);
}
try {
const stats = await fs.stat(filePath);
// Check file size
if (stats.size > this.maxFileSize) {
throw new Error(`File too large: ${stats.size} bytes (max: ${this.maxFileSize})`);
}
const content = await fs.readFile(filePath, 'utf8');
const mimeType = this.getMimeType(filePath);
return {
uri,
mimeType,
text: content,
};
} catch (error) {
throw new Error(`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private async walkDirectory(dir: string): Promise<string[]> {
const files: string[] = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip hidden directories
if (!entry.name.startsWith('.')) {
files.push(...await this.walkDirectory(fullPath));
}
} else if (entry.isFile()) {
const ext = path.extname(entry.name).toLowerCase();
if (this.allowedExtensions.includes(ext)) {
files.push(fullPath);
}
}
}
} catch (error) {
console.error(`Error walking directory ${dir}:`, error);
}
return files;
}
private getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes: { [key: string]: string } = {
'.md': 'text/markdown',
'.txt': 'text/plain',
'.json': 'application/json',
'.yaml': 'application/yaml',
'.yml': 'application/yaml',
};
return mimeTypes[ext] || 'text/plain';
}
private formatFileSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error(`File System Resource Server running (root: ${this.rootDir})`);
}
}
// Main entry point
async function main() {
const server = new FileSystemResourceServer();
await server.start();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Exercise 2: REST API Resource Server¶
Create a tool that:
- Exposes REST API endpoints as resources
- Supports query parameters in URIs
- Handles authentication and rate limiting
- Caches responses appropriately
💡 Solution: REST API Resource Server
Tool Schema - Add this to your tools array:{
name: "create_api_resource_server",
description: "Create an MCP server that exposes REST API endpoints as resources",
inputSchema: {
type: "object",
properties: {
baseUrl: {
type: "string",
description: "Base URL of the API to expose",
format: "uri"
},
apiKey: {
type: "string",
description: "API key for authentication"
},
cacheEnabled: {
type: "boolean",
description: "Enable response caching",
default: true
},
cacheTTL: {
type: "number",
description: "Cache TTL in seconds",
default: 300
},
rateLimit: {
type: "number",
description: "Requests per minute limit",
default: 60
}
},
required: ["baseUrl"]
}
}
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListResourceTemplatesRequestSchema,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/types.js";
import axios, { AxiosResponse } from 'axios';
interface CacheEntry {
data: any;
timestamp: number;
ttl: number;
}
class APIResourceServer {
private server: Server;
private baseUrl: string;
private apiKey?: string;
private cache: Map<string, CacheEntry> = new Map();
private cacheEnabled: boolean;
private cacheTTL: number;
private rateLimit: number;
private requestCounts: Map<string, { count: number; resetTime: number }> = new Map();
constructor(baseUrl: string, apiKey?: string, cacheEnabled: boolean = true, cacheTTL: number = 300, rateLimit: number = 60) {
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
this.apiKey = apiKey;
this.cacheEnabled = cacheEnabled;
this.cacheTTL = cacheTTL;
this.rateLimit = rateLimit;
this.server = new Server(
{
name: "api-resource-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
this.startCacheCleanup();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
const templates: ResourceTemplate[] = [
{
uriTemplate: "api://users/{userId}",
name: "User Profile",
description: "User profile data from API",
mimeType: "application/json",
},
{
uriTemplate: "api://posts/{postId}",
name: "Post Details",
description: "Post details from API",
mimeType: "application/json",
},
{
uriTemplate: "api://search?q={query}&type={type}",
name: "Search Results",
description: "Search API results",
mimeType: "application/json",
},
];
return { resourceTemplates: templates };
});
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
// For demonstration, we'll provide some example resources
// In a real implementation, you might fetch these from the API
const resources = [
{
uri: "api://status",
name: "API Status",
description: "Current API status and health",
mimeType: "application/json",
},
{
uri: "api://info",
name: "API Information",
description: "API metadata and capabilities",
mimeType: "application/json",
},
];
return { resources };
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const content = await this.fetchAPIResource(uri);
return { contents: [content] };
});
}
private async fetchAPIResource(uri: string): Promise<any> {
// Check rate limit
if (!this.checkRateLimit()) {
throw new Error("Rate limit exceeded. Please try again later.");
}
// Check cache first
if (this.cacheEnabled) {
const cached = this.getCached(uri);
if (cached) {
return {
uri,
mimeType: "application/json",
text: JSON.stringify({
...cached,
cached: true,
cacheAge: Math.floor((Date.now() - cached._cacheTimestamp) / 1000),
}, null, 2),
};
}
}
try {
const apiPath = this.uriToAPIPath(uri);
const url = `${this.baseUrl}${apiPath}`;
const headers: any = {
'User-Agent': 'MCP-API-Resource-Server/1.0',
};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
// Or: headers['X-API-Key'] = this.apiKey;
}
const response: AxiosResponse = await axios.get(url, {
headers,
timeout: 10000,
});
const data = {
...response.data,
_metadata: {
statusCode: response.status,
url: url,
fetchedAt: new Date().toISOString(),
contentType: response.headers['content-type'],
}
};
// Cache the response
if (this.cacheEnabled) {
this.setCache(uri, data);
}
return {
uri,
mimeType: "application/json",
text: JSON.stringify(data, null, 2),
};
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
throw new Error(`API Error ${error.response.status}: ${error.response.statusText}`);
} else if (error.code === 'ECONNREFUSED') {
throw new Error(`Cannot connect to API: ${this.baseUrl}`);
} else {
throw new Error(`Network error: ${error.message}`);
}
} else {
throw new Error(`Failed to fetch API resource: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
private uriToAPIPath(uri: string): string {
if (!uri.startsWith('api://')) {
throw new Error(`Invalid API URI: ${uri}`);
}
const path = uri.substring('api://'.length);
// Handle special cases
if (path === 'status') return '/status';
if (path === 'info') return '/info';
// Handle template URIs
if (path.startsWith('users/')) {
const userId = path.substring('users/'.length);
return `/users/${userId}`;
}
if (path.startsWith('posts/')) {
const postId = path.substring('posts/'.length);
return `/posts/${postId}`;
}
if (path.startsWith('search?')) {
// Convert query parameters
const queryString = path.substring('search?'.length);
return `/search?${queryString}`;
}
// Default: use path as-is
return `/${path}`;
}
private checkRateLimit(): boolean {
const now = Date.now();
const windowMs = 60000; // 1 minute
const key = 'global'; // In a real app, use user/session ID
const record = this.requestCounts.get(key);
if (!record || now > record.resetTime) {
this.requestCounts.set(key, { count: 1, resetTime: now + windowMs });
return true;
}
if (record.count >= this.rateLimit) {
return false;
}
record.count++;
return true;
}
private getCached(uri: string): any | null {
const cached = this.cache.get(uri);
if (!cached) return null;
if (Date.now() - cached.timestamp > cached.ttl * 1000) {
this.cache.delete(uri);
return null;
}
return cached.data;
}
private setCache(uri: string, data: any): void {
this.cache.set(uri, {
data: { ...data, _cacheTimestamp: Date.now() },
timestamp: Date.now(),
ttl: this.cacheTTL,
});
}
private startCacheCleanup(): void {
// Clean up expired cache entries every 5 minutes
setInterval(() => {
const now = Date.now();
for (const [uri, entry] of this.cache.entries()) {
if (now - entry.timestamp > entry.ttl * 1000) {
this.cache.delete(uri);
}
}
}, 5 * 60 * 1000);
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error(`API Resource Server running (API: ${this.baseUrl})`);
}
}
// Main entry point
async function main() {
// Example: JSONPlaceholder API
const server = new APIResourceServer('https://jsonplaceholder.typicode.com');
await server.start();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Exercise 3: Database Resource Server¶
Create a tool that:
- Exposes database tables as resources
- Supports parameterized queries via URI templates
- Implements read-only access for security
- Provides metadata about table schemas
💡 Solution: Database Resource Server
Tool Schema - Add this to your tools array:{
name: "create_database_resource_server",
description: "Create an MCP server that exposes database tables as resources",
inputSchema: {
type: "object",
properties: {
databasePath: {
type: "string",
description: "Path to SQLite database file",
default: "./data.db"
},
allowedTables: {
type: "array",
description: "Tables to expose as resources",
items: { type: "string" },
default: []
},
readOnly: {
type: "boolean",
description: "Enforce read-only access",
default: true
},
maxRows: {
type: "number",
description: "Maximum rows to return per query",
default: 1000
}
},
required: ["databasePath"]
}
}
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListResourceTemplatesRequestSchema,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/types.js";
import Database from 'better-sqlite3';
import * as fs from 'fs/promises';
import * as path from 'path';
interface TableInfo {
name: string;
columns: Array<{
name: string;
type: string;
notnull: number;
pk: number;
}>;
rowCount: number;
}
class DatabaseResourceServer {
private server: Server;
private dbPath: string;
private allowedTables: string[];
private readOnly: boolean;
private maxRows: number;
private db: Database.Database | null = null;
constructor(dbPath: string = './data.db', allowedTables: string[] = [], readOnly: boolean = true, maxRows: number = 1000) {
this.dbPath = path.resolve(dbPath);
this.allowedTables = allowedTables;
this.readOnly = readOnly;
this.maxRows = maxRows;
this.server = new Server(
{
name: "database-resource-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
const templates: ResourceTemplate[] = [
{
uriTemplate: "db://tables/{tableName}/schema",
name: "Table Schema",
description: "Schema information for a database table",
mimeType: "application/json",
},
{
uriTemplate: "db://tables/{tableName}/data?limit={limit}&offset={offset}",
name: "Table Data",
description: "Data from a database table with pagination",
mimeType: "application/json",
},
{
uriTemplate: "db://tables/{tableName}/records/{id}",
name: "Table Record",
description: "Specific record from a database table",
mimeType: "application/json",
},
{
uriTemplate: "db://query?sql={sql}¶ms={params}",
name: "Custom Query",
description: "Execute a custom SELECT query",
mimeType: "application/json",
},
];
return { resourceTemplates: templates };
});
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources: any[] = [];
try {
await this.ensureDatabase();
const tables = this.getTableInfo();
for (const table of tables) {
// Table schema resource
resources.push({
uri: `db://tables/${table.name}/schema`,
name: `${table.name} Schema`,
description: `Schema for table ${table.name} (${table.columns.length} columns)`,
mimeType: "application/json",
});
// Table data resource
resources.push({
uri: `db://tables/${table.name}/data?limit=100`,
name: `${table.name} Data`,
description: `Data from table ${table.name} (${table.rowCount} rows)`,
mimeType: "application/json",
});
}
// Database info resource
resources.push({
uri: "db://info",
name: "Database Info",
description: "Database metadata and statistics",
mimeType: "application/json",
});
} catch (error) {
console.error('Error listing resources:', error);
}
return { resources };
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const content = await this.readDatabaseResource(uri);
return { contents: [content] };
});
}
private async ensureDatabase(): Promise<void> {
if (this.db) return;
// Check if database file exists
try {
await fs.access(this.dbPath, fs.constants.R_OK);
} catch {
throw new Error(`Database file not found: ${this.dbPath}`);
}
// Open database in read-only mode if specified
this.db = new Database(this.dbPath, { readonly: this.readOnly });
}
private getTableInfo(): TableInfo[] {
if (!this.db) throw new Error('Database not initialized');
const tables: TableInfo[] = [];
// Get all tables
const tableNames = this.db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`).all() as Array<{ name: string }>;
for (const { name } of tableNames) {
// Check if table is allowed
if (this.allowedTables.length > 0 && !this.allowedTables.includes(name)) {
continue;
}
// Get column info
const columns = this.db.prepare(`PRAGMA table_info(${name})`).all() as any[];
// Get row count
const rowCount = (this.db.prepare(`SELECT COUNT(*) as count FROM ${name}`).get() as any).count;
tables.push({
name,
columns: columns.map(col => ({
name: col.name,
type: col.type,
notnull: col.notnull,
pk: col.pk,
})),
rowCount,
});
}
return tables;
}
private async readDatabaseResource(uri: string): Promise<any> {
await this.ensureDatabase();
if (!uri.startsWith('db://')) {
throw new Error(`Invalid database URI: ${uri}`);
}
const path = uri.substring('db://'.length);
if (path === 'info') {
return this.getDatabaseInfo();
}
if (path.startsWith('tables/')) {
return this.readTableResource(path.substring('tables/'.length));
}
if (path.startsWith('query?')) {
return this.executeCustomQuery(path.substring('query?'.length));
}
throw new Error(`Unknown database resource: ${uri}`);
}
private getDatabaseInfo(): any {
if (!this.db) throw new Error('Database not initialized');
const tables = this.getTableInfo();
const dbStats = this.db.prepare(`
SELECT
COUNT(*) as tableCount,
SUM(
(SELECT COUNT(*) FROM sqlite_master WHERE type='index') +
(SELECT COUNT(*) FROM pragma_table_info(name))
) as totalColumns
FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`).get() as any;
return {
uri: "db://info",
mimeType: "application/json",
text: JSON.stringify({
database: {
path: this.dbPath,
readOnly: this.readOnly,
tableCount: dbStats.tableCount,
totalColumns: dbStats.totalColumns,
},
tables: tables.map(t => ({
name: t.name,
columns: t.columns.length,
rows: t.rowCount,
primaryKey: t.columns.find(c => c.pk)?.name,
})),
server: {
maxRows: this.maxRows,
allowedTables: this.allowedTables.length > 0 ? this.allowedTables : 'all',
},
}, null, 2),
};
}
private readTableResource(tablePath: string): any {
if (!this.db) throw new Error('Database not initialized');
const parts = tablePath.split('/');
const tableName = parts[0];
const action = parts[1];
// Validate table access
if (this.allowedTables.length > 0 && !this.allowedTables.includes(tableName)) {
throw new Error(`Access denied to table: ${tableName}`);
}
switch (action) {
case 'schema':
return this.getTableSchema(tableName);
case 'data':
const url = new URL(`db://tables/${tablePath}`);
const limit = parseInt(url.searchParams.get('limit') || '100');
const offset = parseInt(url.searchParams.get('offset') || '0');
return this.getTableData(tableName, limit, offset);
case 'records':
const recordId = parts[2];
return this.getTableRecord(tableName, recordId);
default:
throw new Error(`Unknown table action: ${action}`);
}
}
private getTableSchema(tableName: string): any {
if (!this.db) throw new Error('Database not initialized');
const tables = this.getTableInfo();
const table = tables.find(t => t.name === tableName);
if (!table) {
throw new Error(`Table not found: ${tableName}`);
}
return {
uri: `db://tables/${tableName}/schema`,
mimeType: "application/json",
text: JSON.stringify({
table: tableName,
columns: table.columns,
rowCount: table.rowCount,
indexes: this.getTableIndexes(tableName),
}, null, 2),
};
}
private getTableData(tableName: string, limit: number, offset: number): any {
if (!this.db) throw new Error('Database not initialized');
// Validate limits
const actualLimit = Math.min(limit, this.maxRows);
const actualOffset = Math.max(0, offset);
const query = `SELECT * FROM ${tableName} LIMIT ? OFFSET ?`;
const rows = this.db.prepare(query).all(actualLimit, actualOffset) as any[];
return {
uri: `db://tables/${tableName}/data?limit=${actualLimit}&offset=${actualOffset}`,
mimeType: "application/json",
text: JSON.stringify({
table: tableName,
query: {
limit: actualLimit,
offset: actualOffset,
totalRows: rows.length,
},
data: rows,
}, null, 2),
};
}
private getTableRecord(tableName: string, recordId: string): any {
if (!this.db) throw new Error('Database not initialized');
// Get primary key column
const columns = this.db.prepare(`PRAGMA table_info(${tableName})`).all() as any[];
const pkColumn = columns.find(col => col.pk === 1);
if (!pkColumn) {
throw new Error(`No primary key found for table: ${tableName}`);
}
const query = `SELECT * FROM ${tableName} WHERE ${pkColumn.name} = ?`;
const row = this.db.prepare(query).get(recordId);
if (!row) {
throw new Error(`Record not found: ${tableName}.${pkColumn.name} = ${recordId}`);
}
return {
uri: `db://tables/${tableName}/records/${recordId}`,
mimeType: "application/json",
text: JSON.stringify({
table: tableName,
primaryKey: {
column: pkColumn.name,
value: recordId,
},
data: row,
}, null, 2),
};
}
private executeCustomQuery(queryString: string): any {
if (!this.db) throw new Error('Database not initialized');
const url = new URL(`db://query?${queryString}`);
const sql = url.searchParams.get('sql');
const params = url.searchParams.get('params');
if (!sql) {
throw new Error('SQL parameter required for custom query');
}
// Security: Only allow SELECT queries
if (!sql.trim().toUpperCase().startsWith('SELECT')) {
throw new Error('Only SELECT queries are allowed for security');
}
let queryParams: any[] = [];
if (params) {
try {
queryParams = JSON.parse(params);
} catch {
throw new Error('Invalid params JSON');
}
}
const stmt = this.db.prepare(sql + ' LIMIT ?');
const rows = stmt.all(...queryParams, this.maxRows);
return {
uri: `db://query?sql=${encodeURIComponent(sql)}¶ms=${encodeURIComponent(JSON.stringify(queryParams))}`,
mimeType: "application/json",
text: JSON.stringify({
query: sql,
parameters: queryParams,
resultCount: rows.length,
data: rows,
}, null, 2),
};
}
private getTableIndexes(tableName: string): any[] {
if (!this.db) return [];
try {
return this.db.prepare(`
SELECT name, sql
FROM sqlite_master
WHERE type='index' AND tbl_name=?
`).all(tableName) as any[];
} catch {
return [];
}
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
if (this.db) {
this.db.close();
}
await this.server.close();
process.exit(0);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error(`Database Resource Server running (DB: ${this.dbPath})`);
}
}
// Main entry point
async function main() {
const server = new DatabaseResourceServer();
await server.start();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
sqlite3 test.db << 'EOF'
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL,
category TEXT,
in_stock BOOLEAN DEFAULT 1
);
INSERT INTO users (name, email, age) VALUES
('Alice Johnson', 'alice@example.com', 28),
('Bob Smith', 'bob@example.com', 34);
INSERT INTO products (name, price, category, in_stock) VALUES
('Laptop', 999.99, 'Electronics', 1),
('Book', 19.99, 'Education', 1);
.quit
EOF
// Database info
{ uri: "db://info" }
// Table schemas
{ uri: "db://tables/users/schema" }
{ uri: "db://tables/products/schema" }
// Table data
{ uri: "db://tables/users/data?limit=10" }
{ uri: "db://tables/products/data?limit=10" }
// Specific records
{ uri: "db://tables/users/records/1" }
{ uri: "db://tables/products/records/2" }
// Custom query
{ uri: "db://query?sql=SELECT * FROM users WHERE age > ?¶ms=[25]" }
Key Takeaways¶
✅ Resources provide passive data access for LLMs
✅ Tools perform active operations with side effects
✅ URI schemes should be hierarchical and descriptive
✅ Templates enable parameterized resource access
✅ Subscriptions support real-time data updates
✅ Security is critical for resource access control
✅ Caching improves performance for static resources
✅ Hybrid servers combine resources and tools effectively
Next Steps¶
In Lab 5, you’ll complete your MCP mastery by learning about Prompts:
- Creating reusable prompt templates
- Embedding resources in prompts
- Supporting prompt arguments
- Building complete, production-ready MCP servers
Ready to add prompts to your MCP toolkit? Continue to Lab 5!
Lab 5: MCP Prompts and Integration¶
Overview¶
Congratulations on reaching the final lab!
As you have already mastered tools and resources, now it’s time to complete your MCP expertise with Prompts.
Prompts are reusable templates that help users and LLMs perform common tasks consistently and effectively. They can embed resources, accept arguments, and create structured workflows that combine the best of human expertise with AI capabilities.
In this lab, you’ll learn how to create sophisticated prompt templates, integrate them with resources and tools, and build complete, production-ready MCP servers that showcase all three capabilities working together.
Learning Objectives¶
By the end of this lab, you will:
- Understand the role of prompts in MCP ecosystems
- Create static and dynamic prompt templates
- Embed resources and arguments in prompts
- Implement prompt handlers with proper validation
- Build complete MCP servers combining all capabilities
- Apply production best practices for deployment
- Debug and troubleshoot complex MCP integrations
- Create reusable prompt libraries for common tasks
Prerequisites¶
- Completed Lab 4 - Implementing MCP Resources
- Understanding of prompt engineering concepts
- Familiarity with template systems and variable substitution
- Experience with complex application architecture
What Makes Prompts Special?¶
Prompts in MCP are more than just text templates - they’re structured, reusable AI workflows that:
- Standardize common tasks across different users and contexts
- Combine expertise from domain specialists with AI capabilities
- Integrate resources to provide rich context automatically
- Accept parameters to customize behavior dynamically
- Create consistency in AI interactions and outputs
When to Use Prompts vs. Tools¶
| Use Case | Use Prompt | Use Tool | Why |
|---|---|---|---|
| Code review | ✅ Prompt | ❌ Tool | Needs structured guidance and context |
| Data analysis | ✅ Prompt | ❌ Tool | Requires analytical reasoning framework |
| Content writing | ✅ Prompt | ❌ Tool | Benefits from style guides and examples |
| API calls | ❌ Prompt | ✅ Tool | Direct action with predictable results |
| Calculations | ❌ Prompt | ✅ Tool | Mathematical precision required |
| Research synthesis | ✅ Prompt | ❌ Tool | Complex reasoning and integration needed |
Prompt Architecture¶
interface Prompt {
name: string; // Unique identifier
description: string; // What the prompt does
arguments?: Argument[]; // Optional parameters
messages: Message[]; // The actual prompt content
}
interface Argument {
name: string; // Parameter name
description: string; // What it controls
required?: boolean; // Is it mandatory?
}
interface Message {
role: "user" | "assistant"; // Who says this
content: Content; // The message content
}
Project Setup¶
Step 1: Create Your Project¶
Let’s start by creating a new MCP server project for prompts:
mkdir my-mcp-prompts-server # <-- next to the directory created in previous labs
cd my-mcp-prompts-server
npm init -y
Step 2: Install Dependencies¶
# Core MCP SDK
npm install @modelcontextprotocol/sdk
# TypeScript and development tools
npm install -D typescript @types/node tsx
Step 3: Configure TypeScript¶
Create a tsconfig.json file with the following content inside the my-mcp-prompts-server directory you have just created:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Step 4: Create Project Structure¶
mkdir src # <-- inside "my-mcp-prompts-server" directory
touch src/index.ts # and leave it empty for now
Creating Your First Prompt Server¶
Step 1: Basic Server Setup¶
Let’s create a basic MCP server that exposes prompts. Paste the following inside src/index.ts:
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
class PromptServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: "mcp-prompts-server",
version: "1.0.0",
},
{
capabilities: {
prompts: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
private setupHandlers(): void {
// List available prompts
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "hello-prompt",
description: "A simple greeting prompt",
},
{
name: "code-review",
description: "Comprehensive code review with best practices",
arguments: [
{
name: "language",
description: "Programming language (e.g., typescript, python)",
required: true,
},
],
},
],
};
});
// Get specific prompt
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
return this.generatePrompt(name, args || {});
});
}
private generatePrompt(name: string, args: Record<string, any>): any {
switch (name) {
case "hello-prompt":
return {
description: "A simple greeting prompt",
messages: [
{
role: "user",
content: {
type: "text",
text: "Hello! Please introduce yourself and explain what you can help me with today.",
},
},
],
};
case "code-review":
const language = args.language || "typescript";
return {
description: `Code review for ${language}`,
messages: [
{
role: "user",
content: {
type: "text",
text: `Please perform a comprehensive code review of the following ${language} code. Focus on:
## Code Quality & Best Practices
- **Readability**: Is the code easy to understand?
- **Maintainability**: How easy will this be to modify later?
- **Performance**: Are there any obvious performance issues?
- **Error Handling**: Are errors handled appropriately?
## ${language.toUpperCase()}-Specific Checks
- Follow ${language} conventions and best practices
- Use appropriate design patterns
- Ensure proper type safety (if applicable)
Please provide specific, actionable feedback with examples where possible.`,
},
},
],
};
default:
throw new Error(`Unknown prompt: ${name}`);
}
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Prompt Server running on stdio");
}
}
// Main entry point
async function main() {
const server = new PromptServer();
await server.start();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Step 2: Test Your Server¶
Build and run the server:
The MCP Inspector will launch a web interface.
Test the prompts in the MCP Inspector UI:
-
List prompts: Click the “Prompts” tab, then click “List Prompts” to see your two prompts (“hello-prompt” and “code-review”).
-
Get hello-prompt: In the same section, select “Get Prompt”, enter “hello-prompt” as the name, and submit to see the greeting prompt content.
-
Get code-review: Select “Get Prompt”, enter “code-review” as the name, and optionally provide arguments like {“language”: “typescript”} in the arguments field, then submit to see the code review prompt.
Step 3: Adding Arguments and Validation¶
Let’s enhance our server with better argument handling. Update your existing src/index.ts file to add more arguments to the code-review prompt and include helper methods for dynamic content generation.
Update the code-review prompt definition in the ListPromptsRequestSchema handler:
// Update the code-review prompt definition
{
name: "code-review",
description: "Comprehensive code review with best practices",
arguments: [
{
name: "language",
description: "Programming language (e.g., typescript, python, java)",
required: true,
},
{
name: "complexity",
description: "Code complexity level (beginner, intermediate, advanced)",
required: false,
},
{
name: "focus",
description: "Review focus areas (comma-separated: quality,security,performance)",
required: false,
},
],
}
Update the prompt generation:
case "code-review":
const language = args.language || "typescript";
const complexity = args.complexity || "intermediate";
const focus = args.focus || "quality";
return {
description: `Code review for ${language} (${complexity} level, focus: ${focus})`,
messages: [
{
role: "user",
content: {
type: "text",
text: `Please perform a comprehensive code review of the following ${language} code.
**Complexity Level:** ${complexity}
**Focus Areas:** ${focus}
## Review Guidelines
### Code Quality & Best Practices
- **Readability**: Is the code easy to understand?
- **Maintainability**: How easy will this be to modify later?
- **Performance**: Are there any obvious performance issues?
### ${complexity.charAt(0).toUpperCase() + complexity.slice(1)} Level Expectations
${this.getComplexityExpectations(complexity)}
### Focus Area: ${focus.toUpperCase()}
${this.getFocusAreaGuidance(focus)}
Please provide specific, actionable feedback with examples where possible.`,
},
},
],
};
Add helper methods:
private getComplexityExpectations(level: string): string {
const expectations: { [key: string]: string } = {
beginner: `- Clear, self-documenting code\n- Basic error handling\n- Simple, understandable logic`,
intermediate: `- Good separation of concerns\n- Comprehensive error handling\n- Appropriate design patterns\n- Unit test coverage`,
advanced: `- High performance and scalability\n- Complex architectural patterns\n- Extensive testing (unit, integration, e2e)\n- Advanced optimization techniques`,
};
return expectations[level] || "- Standard coding practices";
}
private getFocusAreaGuidance(focus: string): string {
const guidance: { [key: string]: string } = {
quality: `**Code Quality Focus:**
- Code readability and maintainability
- Consistent naming conventions
- Proper code organization
- Documentation quality`,
security: `**Security Focus:**
- Input validation and sanitization
- Authentication and authorization
- Data protection practices
- Common vulnerability patterns`,
performance: `**Performance Focus:**
- Algorithm efficiency
- Memory usage optimization
- Database query optimization
- Caching strategies`,
};
return guidance[focus] || "- General best practices";
}
Here’s the complete updated src/index.ts with all the above enhancements, just for review purposes:
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
class PromptServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: "mcp-prompts-server",
version: "1.0.0",
},
{
capabilities: {
prompts: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
private setupHandlers(): void {
// List available prompts
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "hello-prompt",
description: "A simple greeting prompt",
},
{
name: "code-review",
description: "Comprehensive code review with best practices",
arguments: [
{
name: "language",
description: "Programming language (e.g., typescript, python, java)",
required: true,
},
{
name: "complexity",
description: "Code complexity level (beginner, intermediate, advanced)",
required: false,
},
{
name: "focus",
description: "Review focus areas (comma-separated: quality,security,performance)",
required: false,
},
],
},
],
};
});
// Get specific prompt
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
return this.generatePrompt(name, args || {});
});
}
private generatePrompt(name: string, args: Record<string, any>): any {
switch (name) {
case "hello-prompt":
return {
description: "A simple greeting prompt",
messages: [
{
role: "user",
content: {
type: "text",
text: "Hello! Please introduce yourself and explain what you can help me with today.",
},
},
],
};
case "code-review":
const language = args.language || "typescript";
const complexity = args.complexity || "intermediate";
const focus = args.focus || "quality";
return {
description: `Code review for ${language} (${complexity} level, focus: ${focus})`,
messages: [
{
role: "user",
content: {
type: "text",
text: `Please perform a comprehensive code review of the following ${language} code.
**Complexity Level:** ${complexity}
**Focus Areas:** ${focus}
## Review Guidelines
### Code Quality & Best Practices
- **Readability**: Is the code easy to understand?
- **Maintainability**: How easy will this be to modify later?
- **Performance**: Are there any obvious performance issues?
### ${complexity.charAt(0).toUpperCase() + complexity.slice(1)} Level Expectations
${this.getComplexityExpectations(complexity)}
### Focus Area: ${focus.toUpperCase()}
${this.getFocusAreaGuidance(focus)}
Please provide specific, actionable feedback with examples where possible.`,
},
},
],
};
default:
throw new Error(`Unknown prompt: ${name}`);
}
}
private getComplexityExpectations(level: string): string {
const expectations: { [key: string]: string } = {
beginner: `- Clear, self-documenting code\n- Basic error handling\n- Simple, understandable logic`,
intermediate: `- Good separation of concerns\n- Comprehensive error handling\n- Appropriate design patterns\n- Unit test coverage`,
advanced: `- High performance and scalability\n- Complex architectural patterns\n- Extensive testing (unit, integration, e2e)\n- Advanced optimization techniques`,
};
return expectations[level] || "- Standard coding practices";
}
private getFocusAreaGuidance(focus: string): string {
const guidance: { [key: string]: string } = {
quality: `**Code Quality Focus:**
- Code readability and maintainability
- Consistent naming conventions
- Proper code organization
- Documentation quality`,
security: `**Security Focus:**
- Input validation and sanitization
- Authentication and authorization
- Data protection practices
- Common vulnerability patterns`,
performance: `**Performance Focus:**
- Algorithm efficiency
- Memory usage optimization
- Database query optimization
- Caching strategies`,
};
return guidance[focus] || "- General best practices";
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Prompt Server running on stdio");
}
}
// Main entry point
async function main() {
const server = new PromptServer();
await server.start();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Test Enhanced Prompts¶
Test with different arguments:
-
Basic code review (with default values):
Expected: Returns a prompt with intermediate complexity and quality focus. -
Advanced code review with focus:
Expected: Returns a prompt with advanced complexity expectations and security focus guidance.
How to test in MCP Inspector:
-
Start the server:
-
Inside MCP Inspector UI, navigate to Prompts:
- Click on the “Prompts” tab in the Inspector interface
-
Test List Prompts:
- Click “List Prompts” button
- Verify you see “hello-prompt” and “code-review” with the updated arguments
-
Test Get Prompt - Basic:
- Select “Get Prompt” from the dropdown or click the button
- Enter
code-reviewin the “Name” field - In the “Arguments” field, enter:
{"language": "typescript"} - Click “Send Request”
- Check that the response includes intermediate complexity and quality focus
-
Test Get Prompt - Advanced:
- Select “Get Prompt” again
- Enter
code-reviewin the “Name” field - In the “Arguments” field, enter:
- Click “Send Request”
- Verify the prompt content includes advanced expectations and security guidance
-
Test without arguments:
- Try
code-reviewwith no arguments - Should use defaults: typescript, intermediate, quality
- Try
Expected Results:
- The prompt description should reflect the arguments (e.g., “Code review for python (advanced level, focus: security)”)
- The prompt content should include the appropriate complexity expectations and focus area guidance
- Helper methods should dynamically insert the correct text based on arguments
Integrating Resources with Prompts¶
Prompts become more powerful when integrated with resources!
This allows prompts to dynamically include contextual information from your knowledge base, documentation, or data sources, creating richer and more informed AI interactions.
Step 1: Add Resource Support¶
Create a new project for the resource-prompt server:
# Create new directory next to your previous server
mkdir my-resource-prompt-server # <-- next to the directory created previously
cd my-resource-prompt-server
# Initialize and install dependencies
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx
# Create TypeScript config (same as before)
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
EOF
# Create directory structure
mkdir src # <-- inside "my-resource-prompt-server" directory
Now create the server file src/index.ts with the following content that combines prompts with resources:
import {
ListPromptsRequestSchema,
GetPromptRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
class ResourcePromptServer {
private server: Server;
private knowledgeBase: Map<string, any> = new Map();
constructor() {
this.server = new Server(
{
name: "resource-prompt-server",
version: "1.0.0",
},
{
capabilities: {
prompts: {},
resources: {},
},
}
);
this.initializeKnowledgeBase();
this.setupHandlers();
this.setupErrorHandling();
}
private initializeKnowledgeBase(): void {
this.knowledgeBase.set("best-practices", {
title: "Development Best Practices",
content: `
## Code Quality
- Write self-documenting code
- Use meaningful variable names
- Keep functions small and focused
## Testing
- Write unit tests for all functions
- Include integration tests
- Test edge cases and error conditions
`,
});
this.knowledgeBase.set("security-guidelines", {
title: "Security Guidelines",
content: `
## Input Validation
- Validate all user inputs
- Use parameterized queries
- Sanitize HTML content
## Authentication
- Use strong password policies
- Implement multi-factor authentication
`,
});
}
private setupHandlers(): void {
// Resource handlers
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources = Array.from(this.knowledgeBase.entries()).map(([key, doc]) => ({
uri: `kb://articles/${key}`,
name: doc.title,
description: `Knowledge base: ${doc.title}`,
mimeType: "text/markdown",
}));
return { resources };
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const content = this.readKnowledgeBase(uri);
return { contents: [content] };
});
// Prompt handlers
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "contextual-code-review",
description: "Code review with integrated best practices",
arguments: [
{
name: "language",
description: "Programming language",
required: true,
},
{
name: "focusAreas",
description: "Focus areas (comma-separated)",
required: false,
},
],
},
],
};
});
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
return this.generatePrompt(name, args || {});
});
}
private readKnowledgeBase(uri: string): any {
const match = uri.match(/^kb:\/\/articles\/(.+)$/);
if (!match) throw new Error(`Invalid knowledge base URI: ${uri}`);
const key = match[1];
const article = this.knowledgeBase.get(key);
if (!article) throw new Error(`Article not found: ${key}`);
return {
uri,
mimeType: "text/markdown",
text: article.content,
};
}
private generatePrompt(name: string, args: Record<string, any>): any {
switch (name) {
case "contextual-code-review":
return this.createContextualCodeReviewPrompt(args);
default:
throw new Error(`Unknown prompt: ${name}`);
}
}
private createContextualCodeReviewPrompt(args: Record<string, any>): any {
const language = args.language || "typescript";
const focusAreas = args.focusAreas ? args.focusAreas.split(',').map((s: string) => s.trim()) : ["quality"];
// Fetch relevant knowledge base articles
let contextContent = "";
if (focusAreas.includes("quality")) {
const bestPractices = this.knowledgeBase.get("best-practices");
if (bestPractices) {
contextContent += `\n## Development Best Practices\n${bestPractices.content}`;
}
}
if (focusAreas.includes("security")) {
const securityGuidelines = this.knowledgeBase.get("security-guidelines");
if (securityGuidelines) {
contextContent += `\n## Security Guidelines\n${securityGuidelines.content}`;
}
}
return {
description: `Contextual code review for ${language} with focus on: ${focusAreas.join(', ')}`,
messages: [
{
role: "user",
content: {
type: "text",
text: `Please perform a comprehensive code review of the following ${language} code. Use the integrated knowledge base context to provide informed recommendations.
## Review Context
${contextContent}
## Code to Review
[Insert code here]
## Review Focus Areas
${focusAreas.map(area => `- **${area.charAt(0).toUpperCase() + area.slice(1)}**: Apply relevant guidelines from the context above`).join('\n')}
## Review Structure
1. **Summary**: Overall assessment and key findings
2. **Strengths**: What the code does well
3. **Areas for Improvement**: Specific recommendations with context references
4. **Action Items**: Prioritized list of recommended changes
Please reference specific guidelines from the provided context and explain how they apply to this code.`,
},
},
],
};
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Resource-Prompt Server running on stdio");
}
}
Step 2: Test Resource Integration¶
Build and run the resource-prompt server:
Test the enhanced server inside the MCP Inspector UI:
-
Navigate to Resources tab:
- Click on the “Resources” tab
-
List resources:
- Click “List Resources” button
- Verify you see the knowledge base articles (“Development Best Practices” and “Security Guidelines”)
-
Read a resource:
- Select “Read Resource” from the dropdown
- Enter
kb://articles/best-practicesin the “URI” field - Click “Send Request”
- Check that the response includes the full content of the best practices article
-
Read another resource:
- Select “Read Resource” again
- Enter
kb://articles/security-guidelinesin the “URI” field - Click “Send Request”
- Verify the security guidelines content is returned
-
Navigate to Prompts tab:
- Click on the “Prompts” tab
-
List prompts:
- Click “List Prompts” button
- Verify you see the “contextual-code-review” prompt with its arguments
-
Get contextual prompt - Basic:
- Select “Get Prompt”
- Enter
contextual-code-reviewin the “Name” field - In the “Arguments” field, enter:
{"language": "typescript"} - Click “Send Request”
- Check that the prompt content includes the best practices from the knowledge base
-
Get contextual prompt - With focus areas:
- Select “Get Prompt” again
- Enter
contextual-code-reviewin the “Name” field - In the “Arguments” field, enter:
- Click “Send Request”
- Verify the prompt includes both best practices and security guidelines from the knowledge base
Expected Results:
- Resources should list the knowledge base articles with proper URIs and descriptions
- Reading resources should return the full markdown content of each article
- Prompts should dynamically include relevant knowledge base content based on the focus areas argument
- The contextual prompt should reference specific guidelines from the integrated resources
Complete MCP Server Integration¶
Complete MCP server integration combines all three core capabilities - tools, resources, and prompts - into a single server that can perform complex operations and provide rich, contextual AI interactions.
Step 1: Combine All Capabilities¶
Create a new project for the complete MCP server:
# Create new directory next to your previous servers
mkdir my-complete-mcp-server
cd my-complete-mcp-server
# Initialize and install dependencies
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx
# Create TypeScript config (same as before)
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
EOF
# Create directory structure
mkdir src
Now create the server file src/index.ts, that combines tools, resources, and prompts, with the following content:
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
class CompleteMCPServer {
private server: Server;
private knowledgeBase: Map<string, any> = new Map();
constructor() {
this.server = new Server(
{
name: "complete-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
}
);
this.initializeKnowledgeBase();
this.setupHandlers();
this.setupErrorHandling();
}
private initializeKnowledgeBase(): void {
this.knowledgeBase.set("development-workflow", {
title: "Complete Development Workflow",
content: `
## Development Process
1. Requirements gathering and analysis
2. System design and architecture
3. Implementation with best practices
4. Code review and quality assurance
5. Testing and validation
6. Deployment and monitoring
7. Maintenance and updates
`,
});
}
private setupHandlers(): void {
// Tool handlers
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "analyze_codebase",
description: "Analyze codebase structure and provide insights",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "Path to analyze" },
analysisType: {
type: "string",
enum: ["structure", "complexity", "dependencies"],
default: "structure"
},
},
required: ["path"],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "analyze_codebase":
return this.analyzeCodebase(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Resource handlers
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources = [
...Array.from(this.knowledgeBase.entries()).map(([key, doc]) => ({
uri: `kb://articles/${key}`,
name: doc.title,
description: `Knowledge: ${doc.title}`,
mimeType: "text/markdown",
})),
];
return { resources };
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const content = await this.readResource(uri);
return { contents: [content] };
});
// Prompt handlers
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "full-development-workflow",
description: "Complete development workflow with integrated resources",
arguments: [
{
name: "projectType",
description: "Type of project (web, api, mobile)",
required: true,
},
],
},
{
name: "code-quality-assessment",
description: "Comprehensive code quality assessment using all capabilities",
arguments: [
{
name: "codeSample",
description: "Code to assess",
required: true,
},
],
},
],
};
});
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
return this.generateIntegratedPrompt(name, args || {});
});
}
private async analyzeCodebase(args: Record<string, any>): Promise<any> {
const targetPath = args.path || "./";
const analysisType = args.analysisType || "structure";
try {
const analysis = await this.performCodeAnalysis(targetPath, analysisType);
return {
content: [
{
type: "text",
text: `## Codebase Analysis: ${analysisType.toUpperCase()}\n\n${analysis}`,
},
],
};
} catch (error) {
throw new Error(`Codebase analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private async performCodeAnalysis(targetPath: string, analysisType: string): Promise<string> {
// Simplified analysis - in real implementation, use proper code analysis tools
return `### Analysis Results
- **Path**: ${targetPath}
- **Type**: ${analysisType}
- **Status**: Analysis completed successfully
### Recommendations
- Consider organizing files by feature
- Add proper documentation
- Implement consistent coding standards`;
}
private async readResource(uri: string): Promise<any> {
if (uri.startsWith('kb://')) {
return this.readKnowledgeBase(uri);
}
throw new Error(`Unknown resource URI: ${uri}`);
}
private readKnowledgeBase(uri: string): any {
const match = uri.match(/^kb:\/\/articles\/(.+)$/);
if (!match) throw new Error(`Invalid knowledge base URI: ${uri}`);
const key = match[1];
const article = this.knowledgeBase.get(key);
if (!article) throw new Error(`Article not found: ${key}`);
return {
uri,
mimeType: "text/markdown",
text: article.content,
};
}
private async generateIntegratedPrompt(name: string, args: Record<string, any>): Promise<any> {
switch (name) {
case "full-development-workflow":
return this.createFullWorkflowPrompt(args);
case "code-quality-assessment":
return this.createQualityAssessmentPrompt(args);
default:
throw new Error(`Unknown prompt: ${name}`);
}
}
private async createFullWorkflowPrompt(args: Record<string, any>): Promise<any> {
const projectType = args.projectType || "web";
const workflowDoc = this.knowledgeBase.get("development-workflow");
return {
description: `Complete ${projectType} development workflow`,
messages: [
{
role: "user",
content: {
type: "text",
text: `Guide me through a complete ${projectType} development project.
## Development Workflow Reference
${workflowDoc ? workflowDoc.content : "Standard development practices apply"}
## Project Context
- **Type**: ${projectType}
- **Current Phase**: Planning
Please provide a comprehensive development plan with specific phases, deliverables, and best practices.`,
},
},
],
};
}
private async createQualityAssessmentPrompt(args: Record<string, any>): Promise<any> {
const codeSample = args.codeSample || "[Insert code here]";
// Use tools to analyze the code
const analysisResult = await this.analyzeCodebase({ path: "./", analysisType: "structure" });
return {
description: "Comprehensive code quality assessment",
messages: [
{
role: "user",
content: {
type: "text",
text: `Perform a comprehensive code quality assessment of the following code.
## Code to Assess
\`\`\`
${codeSample}
\`\`\`
## Automated Analysis Results
${analysisResult.content[0].text}
## Assessment Framework
### 1. Code Quality Metrics
- **Readability**: Is the code easy to understand?
- **Maintainability**: How easy will this be to modify?
- **Performance**: Are there any obvious issues?
### 2. Best Practices
- Does it follow coding standards?
- Are there appropriate error handling?
- Is the code well-structured?
### 3. Recommendations
Provide specific, prioritized recommendations for improvement.
Please provide a thorough assessment.`,
},
},
],
};
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Complete MCP Server running on stdio");
}
}
Step 2: Test Complete Integration¶
Build and run the complete MCP server:
Test the complete integration inside the MCP Inspector UI:
-
Navigate to Tools tab:
- Click on the “Tools” tab
-
List tools:
- Click “List Tools” button
- Verify you see the “analyze_codebase” tool with its description and input schema
-
Call tool:
- Select “Call Tool” from the dropdown
- Enter
analyze_codebasein the “Name” field - In the “Arguments” field, enter:
{"path": "./", "analysisType": "structure"} - Click “Send Request”
- Check that the response includes a codebase analysis with recommendations
-
Navigate to Resources tab:
- Click on the “Resources” tab
-
List resources:
- Click “List Resources” button
- Verify you see the knowledge base article (“Complete Development Workflow”)
-
Read resource:
- Select “Read Resource” from the dropdown
- Enter
kb://articles/development-workflowin the “URI” field - Click “Send Request”
- Check that the response includes the full development workflow content
-
Navigate to Prompts tab:
- Click on the “Prompts” tab
-
List prompts:
- Click “List Prompts” button
- Verify you see the “full-development-workflow” and “code-quality-assessment” prompts with their arguments
-
Get full development workflow prompt:
- Select “Get Prompt”
- Enter
full-development-workflowin the “Name” field - In the “Arguments” field, enter:
{"projectType": "web"} - Click “Send Request”
- Check that the prompt content includes the development workflow from the knowledge base
-
Get code quality assessment prompt:
- Select “Get Prompt” again
- Enter
code-quality-assessmentin the “Name” field - In the “Arguments” field, enter:
{"codeSample": "function test() { return true; }"} - Click “Send Request”
- Verify the prompt includes automated analysis results and assessment framework
Expected Results:
- Tools should list the analysis tool with proper schema and description
- Calling tools should return structured analysis results
- Resources should list knowledge base articles with proper URIs
- Reading resources should return the full markdown content
- Prompts should list integrated prompts with their arguments
- Getting prompts should dynamically include tool results and resource content
- The complete integration should demonstrate all three capabilities working together seamlessly
Hands-On Exercises¶
Exercise 1: Custom Prompt Library¶
Goal: Create a specialized prompt library for a specific domain.
Steps:
- Choose a domain (e.g., data science, DevOps, content writing)
- Create 3-5 domain-specific prompts
- Add appropriate arguments for customization
- Include domain-specific resources
- Test with the MCP Inspector
Requirements:
- At least one prompt with required arguments
- At least one prompt with optional arguments
- Include resource integration
- Proper error handling
Solution
Choose data science domain.Create 3 domain-specific prompts: data-exploration, model-training, visualization.
Add arguments: dataset (required), focus (optional).
Include domain-specific resources: data science best practices.
Example code structure:
// In ListPromptsRequestSchema handler
{
name: "data-exploration",
description: "Guide for exploratory data analysis",
arguments: [
{
name: "dataset",
description: "Dataset description",
required: true,
},
{
name: "focus",
description: "Analysis focus (distribution, correlation, outliers)",
required: false,
},
],
},
{
name: "model-training",
description: "Guide for machine learning model training",
arguments: [
{
name: "algorithm",
description: "ML algorithm (linear, tree, neural)",
required: true,
},
{
name: "target",
description: "Target variable",
required: false,
},
],
},
{
name: "data-visualization",
description: "Guide for creating effective data visualizations",
arguments: [
{
name: "chartType",
description: "Type of chart (bar, line, scatter)",
required: true,
},
],
}
// In generatePrompt method
case "data-exploration":
const dataset = args.dataset || "dataset";
const focus = args.focus || "distribution";
return {
description: `Data exploration for ${dataset} focusing on ${focus}`,
messages: [
{
role: "user",
content: {
type: "text",
text: `Perform exploratory data analysis on the ${dataset} dataset.
Focus areas: ${focus}
Guidelines:
- Examine data structure and types
- Check for missing values and outliers
- Analyze distributions and correlations
- Generate summary statistics
Provide insights and recommendations.`,
},
},
],
};
// Add resources in initializeKnowledgeBase
this.knowledgeBase.set("data-science-practices", {
title: "Data Science Best Practices",
content: `
Data Quality
- Validate data integrity
- Handle missing values appropriately
- Check for data consistency
Analysis Process
- Start with descriptive statistics
- Use visualizations for insights
- Test hypotheses systematically
`,
});
Exercise 2: Multi-Step Workflow Prompt¶
Goal: Create a prompt that guides users through complex, multi-step processes.
Steps:
- Design a multi-step workflow (e.g., code review → testing → deployment)
- Create prompts for each step
- Add logic to chain prompts together
- Include progress tracking
- Test the complete workflow
Requirements:
- Clear step progression
- State management between steps
- Error handling and recovery
- Progress indicators
Solution
Design a code review to deployment workflow with steps: review, testing, deployment.Create prompts for each step with currentStep argument.
Add logic to chain prompts and track progress.
Example code structure:
// In ListPromptsRequestSchema handler
{
name: "workflow-step",
description: "Multi-step development workflow guidance",
arguments: [
{
name: "currentStep",
description: "Current workflow step (review, testing, deployment)",
required: true,
},
{
name: "projectType",
description: "Type of project",
required: false,
},
],
}
// In generatePrompt method
case "workflow-step":
const step = args.currentStep || "review";
const projectType = args.projectType || "web";
const workflowSteps = ["review", "testing", "deployment"];
const currentIndex = workflowSteps.indexOf(step);
return {
description: `${projectType} development workflow - Step ${currentIndex + 1}: ${step}`,
messages: [
{
role: "user",
content: {
type: "text",
text: `Guide for ${projectType} project ${step} phase.
Progress: Step ${currentIndex + 1} of ${workflowSteps.length}
Previous steps: ${workflowSteps.slice(0, currentIndex).join(', ') || 'None'}
Next steps: ${workflowSteps.slice(currentIndex + 1).join(', ') || 'Complete'}
${this.getStepGuidance(step, projectType)}
Provide detailed guidance and check completion criteria before proceeding.`,
},
},
],
};
// Helper method
private getStepGuidance(step: string, projectType: string): string {
const guidance: { [key: string]: { [key: string]: string } } = {
review: {
web: "Code Review Guidelines:\n- Check code quality and standards\n- Verify security practices\n- Review performance considerations\n- Validate functionality",
api: "API Review Guidelines:\n- Check endpoint design\n- Validate error handling\n- Review authentication\n- Test API contracts",
},
testing: {
web: "Testing Guidelines:\n- Unit test coverage\n- Integration testing\n- User acceptance testing\n- Performance testing",
api: "API Testing Guidelines:\n- Endpoint testing\n- Load testing\n- Security testing\n- Contract testing",
},
deployment: {
web: "Deployment Guidelines:\n- Environment configuration\n- Database migrations\n- Rollback procedures\n- Monitoring setup",
api: "API Deployment Guidelines:\n- API gateway configuration\n- Version management\n- Documentation publishing\n- Client notifications",
},
};
return guidance[step]?.[projectType] || "Follow standard practices for this step.";
}
Exercise 3: Production-Ready Server¶
Goal: Create a production-ready MCP server with all capabilities.
Steps:
- Implement comprehensive error handling
- Add logging and monitoring
- Include health checks
- Add configuration management
- Implement rate limiting
- Create deployment scripts
Requirements:
- Structured logging
- Health check endpoints
- Configuration validation
- Graceful shutdown
- Performance monitoring
Solution
Implement comprehensive error handling, logging, health checks, configuration management, rate limiting, and deployment scripts.Example code structure:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
class ProductionMCPServer {
private server: Server;
private config: any;
private requestCount = 0;
private startTime = Date.now();
constructor(configPath?: string) {
this.config = this.loadConfiguration(configPath);
this.server = new Server(
{
name: this.config.name,
version: this.config.version,
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
}
);
this.setupLogging();
this.setupHandlers();
this.setupErrorHandling();
this.setupHealthChecks();
}
private loadConfiguration(configPath?: string): any {
// Load from file or environment
return {
name: "production-mcp-server",
version: "1.0.0",
port: process.env.PORT || 3000,
logLevel: process.env.LOG_LEVEL || "info",
rateLimit: parseInt(process.env.RATE_LIMIT || "100"),
};
}
private setupLogging(): void {
// Use a proper logging library in production
console.log = this.createLogger("info");
console.error = this.createLogger("error");
}
private createLogger(level: string): (...args: any[]) => void {
return (...args: any[]) => {
const timestamp = new Date().toISOString();
process.stderr.write(`[${timestamp}] ${level.toUpperCase()}: ${args.join(' ')}\n`);
};
}
private setupHandlers(): void {
// Add rate limiting to handlers
this.server.setRequestHandler(ListToolsRequestSchema, this.rateLimitedHandler(async () => {
return { tools: [] };
}));
// Add other handlers similarly
}
private rateLimitedHandler(handler: Function): Function {
return async (request: any) => {
this.requestCount++;
if (this.requestCount > this.config.rateLimit) {
throw new Error("Rate limit exceeded");
}
return handler(request);
};
}
private setupHealthChecks(): void {
// Add health check endpoint (if using HTTP transport)
// For stdio, we can add a special tool
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "health_check") {
return {
content: [{
type: "text",
text: JSON.stringify({
status: "healthy",
uptime: Date.now() - this.startTime,
requestCount: this.requestCount,
}),
}],
};
}
// Handle other tools
});
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("Server error:", error);
// Send to monitoring service
};
process.on("SIGTERM", async () => {
console.log("Received SIGTERM, shutting down gracefully");
await this.server.close();
process.exit(0);
});
process.on("uncaughtException", (error) => {
console.error("Uncaught exception:", error);
process.exit(1);
});
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.log("Production MCP Server running on stdio");
}
}
// Deployment script (deploy.sh)
#!/bin/bash
npm run build
npm run test
docker build -t mcp-server .
docker run -p 3000:3000 mcp-server
Key Takeaways¶
✅ Prompts create reusable AI workflows with structured guidance
✅ Resource integration provides contextually rich prompt experiences
✅ Arguments make prompts flexible and customizable
✅ Complete servers combine tools, resources, and prompts effectively
✅ Dynamic prompts adapt to current data and context
✅ Production readiness requires comprehensive error handling and monitoring
✅ Workflow orchestration enables complex, multi-step AI processes
✅ Domain specialization creates powerful, focused AI capabilities
Next Steps¶
Congratulations!
You’ve completed the comprehensive MCP learning series.
You now posses the knowledge and skills to:
✅ Build complete MCP servers with all three capabilities
✅ Create sophisticated AI workflows and integrations
✅ Deploy production-ready MCP solutions
✅ Contribute to the MCP ecosystem
What’s Next?¶
Further experimentation¶
Go ahead and experiment further with MCP un the MCP Lab Tasks section, filled with hands-on exercises to deepen your understanding of MCP capabilities.
Advanced Topics¶
- MCP Extensions: Custom protocol extensions
- Multi-server Coordination: Server orchestration
- Performance Optimization: Scaling MCP servers
- Security Hardening: Advanced security patterns
Real-World Applications¶
- Code Analysis Tools: Automated code review and improvement
- DevOps Automation: Infrastructure and deployment automation
- Data Science Workflows: AI-assisted data analysis
- Content Creation: AI-powered content generation pipelines
Community Engagement¶
- Contribute to MCP: Join the open-source development
- Share Your Servers: Publish your MCP servers
- Build Integrations: Create MCP clients and tools
- Teach Others: Help grow the MCP community
Your MCP journey has just begun!
The protocol provides endless possibilities for AI-human collaboration.
Go forth and build amazing things!
Complete MCP Server - Hands-On Lab¶
MCP Server Structure Lab¶
Lab Objective¶
- In this hands-on lab, you’ll build a complete MCP (Model Context Protocol) server from scratch.
- You’ll learn how each component works by implementing it yourself, understanding why each piece is necessary, and seeing the complete architecture come together.
Prerequisites¶
- Python 3.10 or higher installed
- Basic understanding of Python programming
- Terminal/command line access
- Text editor or IDE
Getting Started¶
Step 1: Create Your Project File¶
- Create a new file called
mcp_server.py - Open it in your favorite text editor
- We’ll build this server step by step!
Step 01: Adding Imports¶
- Before we write any code, we need to understand what libraries we’re using and why.
asyncio - Asynchronous I/O¶
asyncio
Definition:¶
asynciois a Python library for writing concurrent code using the async/await syntax.
Why:¶
- Enables asynchronous programming in Python
- MCP servers handle multiple concurrent operations (I/O, requests) without blocking
Usage:¶
async/awaitkeywords, event loops, concurrent task execution
json - JavaScript Object Notation¶
json
Definition:¶
- JSON encoding/decoding for data serialization
Why:¶
- MCP uses JSON-RPC protocol; resources return JSON data
Usage:¶
json.dumps()to serialize Python dicts,json.loads()to parse
typing - Type Hints¶
typing
Definition:¶
- Type hints for better code documentation and IDE support
Why:¶
- Makes code more maintainable and catches type errors early
Usage:¶
- Function parameters, return types (Any = any type, Optional = can be None)
mcp.server - Core MCP Server¶
mcp.server
Definition:¶
- Core MCP Server class - the foundation of our server
Why:¶
- Provides all MCP protocol implementation and lifecycle management
Usage:¶
- Create server instance, register handlers, manage connections
mcp.server.stdio - Standard I/O Transport¶
mcp.server.stdio
Definition:¶
- Standard Input/Output transport layer for MCP
Why:¶
- MCP servers communicate via stdio (standard in/out streams)
Usage:¶
- Connects server to clients through stdin/stdout pipes
mcp.types - Protocol Types¶
mcp.types
Definition:¶
- MCP protocol type definitions for structured data
Why:¶
- Type-safe definitions for all MCP primitives (tools, resources, prompts)
Usage:¶
- Tool = executable functions, Resource = readable data, Prompt = templates, TextContent = text responses
sys - System Functions¶
sys
Definition:¶
- System-specific parameters and functions
Why:¶
- Handle system exits, command-line arguments, and stdio streams
Usage:¶
sys.exit()for graceful shutdown,sys.stdin/stdoutfor I/O
Step 02: Skeleton Code¶
Skeleton 01: Imports¶
-
Set the following imports inside the
mcp_server.py:#!/usr/bin/env python3 """ Complete MCP (Model Context Protocol) Server Implementation Built step by step for learning purposes. - Enables clients to get ready-to-use prompts - Connects prompt templates to actual content """ import asyncio import json from typing import Any, Optional from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import ( Tool, Resource, Prompt, TextContent, ImageContent, EmbeddedResource, ) import sys -
Create the
requirements.txtfile with the following content: -
Install the MCP library:
Skeleton 02: Class¶
-
Add thislass definition after the imports in your
mcp_server.pyfile:class CompleteMCPServer: """ A comprehensive MCP Server implementation showcasing all - Enables clients to get ready-to-use prompts - Connects prompt templates to actual content protocol features. This class demonstrates: - Server initialization - Tool registration and execution - Resource management - Prompt templates - Defines the behavior of each prompt - Handlers make prompts functional - Request handling """
Skeleton 03: Constructor¶
Method__init__ (Constructor)¶
Capabilities:
- Initializes the MCP Server instance
- Creates the server object with name and version
- Sets up the foundation for all MCP operations
- Enables clients to get ready-to-use prompts
- Connects prompt templates to actual content
- Prepares data structures for tools, resources, and prompts
Why This Runs First:
- Constructor must run before any other methods
- Creates the server object that all other methods will use
- Defines the behavior of each prompt
- Handlers make prompts functional
- No other operations can occur without this initialization
- Sets up the basic state of the server
- Add this class definition after inside your
CompleteMCPServerclass:
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
Code break down:¶
Server("complete-mcp-server")creates the MCP server with a nameself.data_store = {}creates an empty dictionary for storing data- This object will be used throughout all other methods
Skeleton 04: Register Tools¶
Methodregister_tools¶
Capabilities:
- Registers all available tools with the MCP server
- Defines tool schemas (name, description, parameters)
- Makes tools discoverable to clients
- Enables clients to get ready-to-use prompts
- Connects prompt templates to actual content
- Sets up the tool execution infrastructure
Why This Runs Second:
- After server initialization, we need to define what tools are available
- Defines the behavior of each prompt
- Handlers make prompts functional
- Tools must be registered before they can be called
- Defines the capabilities clients can invoke
What is a Tool?
- A tool is an executable function that clients can invoke.
- Think of it like an API endpoint that performs an action.
- Tools have names, descriptions, and input parameters.
- Clients can discover and call these tools to perform operations.
- Examples: calculator, data storage, text processing
- Tools are central to MCP’s functionality.
-
Add this method to your class:
def register_tools(self): """Register all available tools with the MCP server.""" @self.server.list_tools() async def list_tools() -> list[Tool]: """ Return the list of available tools. This is called when clients want to discover what tools are available. """ return [ Tool( name="calculate", description="Perform mathematical operations (add, subtract, multiply, divide)", inputSchema={ "type": "object", "properties": { "operation": { "type": "string", "enum": ["add", "subtract", "multiply", "divide"], "description": "The operation to perform" }, "a": { "type": "number", "description": "First number" }, "b": { "type": "number", "description": "Second number" } }, "required": ["operation", "a", "b"] } ), Tool( name="store_data", description="Store a key-value pair in the server's data store", inputSchema={ "type": "object", "properties": { "key": { "type": "string", "description": "The key to store" }, "value": { "type": "string", "description": "The value to store" } }, "required": ["key", "value"] } ), Tool( name="retrieve_data", description="Retrieve a value from the server's data store", inputSchema={ "type": "object", "properties": { "key": { "type": "string", "description": "The key to retrieve" } }, "required": ["key"] } ), Tool( name="echo", description="Echo back the input text", inputSchema={ "type": "object", "properties": { "text": { "type": "string", "description": "Text to echo back" } }, "required": ["text"] } ) ] print("Tools registered: calculate, store_data, retrieve_data, echo")
What’s Happening:
- The
@self.server.list_tools()decorator registers a handler for tool listing - Each
Toolobject defines the tool’s name, description, and input schema - The
inputSchemauses JSON Schema format to validate inputs - When a client calls
list_tools(), they get this list - This makes the tools discoverable and usable by clients
Hands-On Exercise:¶
- Add a new tool called
greetingthat takes a string inputnameand returns a greeting message. - Define its name, description, and input schema similar to the other tools.
- Test it later when we implement tool handlers.
- Hint: Use the existing tools as a reference for structure.
- After adding, your
list_toolsmethod should include the newgreetingtool. - This exercise helps you understand how to define and register new tools in the MCP server.
- Try to implement it on your own before looking at the solution below!
Solution for Greeting Tool
Skeleton 05: Tool(s) Handlers¶
Methodregister_tool_handlers¶
Capabilities:
- Implements the actual logic for each tool
- Handles tool execution requests from clients
- Processes input parameters and returns results
- Enables clients to get ready-to-use prompts
- Connects prompt templates to actual content
- Provides error handling for tool execution
- Enables dynamic tool functionality
Why This Runs Third:
- After tools are registered, we need to define what happens when
- Defines the behavior of each prompt
- Handlers make prompts functional
each tool is called
- Without handlers, tools are just definitions with no action
- Tools need implementation before they can be executed
- Connects tool schemas to actual functionality
- Defines the behavior of each tool
- Handlers make tools operational
- Clients rely on these handlers to perform tasks
- This is where the server’s capabilities come to life
- Handlers are essential for a functional MCP server
- They bridge the gap between tool definition and execution
-
Add this method to your class:
def register_tool_handlers(self): """Implement the actual logic for each tool.""" @self.server.call_tool() async def call_tool(name: str, arguments: Any) -> list[TextContent]: """ Handle tool execution requests. This is called when a client wants to execute a tool. """ if name == "calculate": operation = arguments.get("operation") a = arguments.get("a") b = arguments.get("b") if operation == "add": result = a + b elif operation == "subtract": result = a - b elif operation == "multiply": result = a * b elif operation == "divide": if b == 0: return [TextContent( type="text", text="Error: Cannot divide by zero" )] result = a / b else: return [TextContent( type="text", text=f"Error: Unknown operation '{operation}'" )] return [TextContent( type="text", text=f"Result: {a} {operation} {b} = {result}" )] elif name == "store_data": key = arguments.get("key") value = arguments.get("value") self.data_store[key] = value return [TextContent( type="text", text=f"Stored: {key} = {value}" )] elif name == "retrieve_data": key = arguments.get("key") value = self.data_store.get(key) if value is None: return [TextContent( type="text", text=f"Error: Key '{key}' not found" )] return [TextContent( type="text", text=f"Retrieved: {key} = {value}" )] elif name == "echo": text = arguments.get("text") return [TextContent( type="text", text=f"Echo: {text}" )] else: return [TextContent( type="text", text=f"Error: Unknown tool '{name}'" )] print("Tool handlers implemented")
What’s Happening:
- The
@self.server.call_tool()decorator registers the execution handler - Each tool’s logic is in an
if/elifblock - Results are wrapped in
TextContentobjects - Error handling is included for edge cases (like division by zero)
- When a client calls a tool, this handler processes the request and returns the output
- This makes the tools functional and usable by clients
Hands-On Exercise:¶
- Implement the handler logic for the
greetingtool you added earlier. - The tool should take the
nameparameter and return a greeting message like “Hello, {name}!”. - Test it later when we run the server.
- Hint: Follow the structure of the other tool handlers.
- After adding, your
call_toolmethod should include the newgreetingtool logic. - This exercise helps you understand how to implement tool functionality in the MCP server.
- Try to implement it on your own before looking at the solution below!
Solution for Greeting Tool Handler
Skeleton 06: Register Resources¶
Methodregister_resources¶
Capabilities:
- Registers resources that clients can access
- Defines resource URIs and metadata
- Makes static and dynamic content available
- Enables clients to get ready-to-use prompts
- Connects prompt templates to actual content
- Enables resource discovery and retrieval
-
Provides additional data for clients
- Supports richer interactions with the server
- Expands server capabilities beyond tools
- Facilitates data sharing and information access
-
Defines the behavior of each prompt
- Handlers make prompts functional
Why This Runs Fourth:
- After tools are set up, we add resources which provide additional data
- Resources are complementary to tools
- Provides data that tools might reference
What is a Resource?
- A resource is readable data or content.
- Think of it like a file or endpoint you can read from (but not execute).
- Resources have URIs (like URLs) and metadata (name, description, MIME type).
- Clients can discover and read these resources.
- Examples: server info, data store contents, welcome message
- Resources enhance the server’s functionality by providing static or dynamic data.
-
Add this method to your class:
def register_resources(self): """Register resources that clients can access.""" @self.server.list_resources() async def list_resources() -> list[Resource]: """ Return the list of available resources. This is called when clients want to discover what resources are available. """ return [ Resource( uri="resource://server-info", name="Server Information", description="Information about this MCP server", mimeType="application/json" ), Resource( uri="resource://data-store", name="Data Store", description="Current contents of the data store", mimeType="application/json" ), Resource( uri="resource://welcome", name="Welcome Message", description="Welcome message and server capabilities", mimeType="text/plain" ) ] print("Resources registered: server-info, data-store, welcome")
What’s Happening:
- The
@self.server.list_resources()decorator registers the resource listing handler - Each
Resourcedefines a URI (like a URL), name, description, and MIME type - URIs use the
resource://scheme to identify resources - When a client calls
list_resources(), they get this list - This makes the resources discoverable and accessible by clients
- Resources provide additional data that clients can read
- Enhances the server’s capabilities beyond just tools
Hands-On Exercise:¶
- Add a new resource called
server-authorthat return your name as the author of the server. - Define its URI, name, description, and MIME type similar to the other resources.
- Test it later when we implement resource handlers.
- Hint: Use the existing resources as a reference for structure.
- After adding, your
list_resourcesmethod should include the newserver-authorresource. - This exercise helps you understand how to define and register new resources in the MCP server.
- Try to implement it on your own before looking at the solution below!
Solution for Server Stats Resource
Skeleton 07: Resource Handlers¶
Methodregister_resource_handlers¶
Capabilities:
- Implements resource retrieval logic
- Returns actual content for each resource
- Handles dynamic resource generation
- Enables clients to get ready-to-use prompts
- Connects prompt templates to actual content
- Provides resource access control
-
Enables clients to read server data
- Supports various content types (JSON, text)
- Facilitates data sharing with clients
- Connects resource definitions to actual data
- Enhances server usability and information access
- Defines the behavior of each prompt
- Handlers make prompts functional
Why This Runs Fifth:
- After resources are registered, we need to implement what content is returned
- Resources need implementation to return actual data
- Connects resource URIs to actual content
- Defines the behavior of each resource
- Handlers make resources accessible
- Clients rely on these handlers to read data
- This is where resource definitions become functional
- Handlers are essential for a usable MCP server
- They bridge the gap between resource definition and content delivery
- Without handlers, resources are just placeholders with no data
- Handlers bring resources to life
-
Add this method to your class:
def register_resource_handlers(self): """Implement resource retrieval logic.""" @self.server.read_resource() async def read_resource(uri: str) -> str: """ Handle resource read requests. This is called when a client wants to read a resource. """ if uri == "resource://server-info": info = { "name": "complete-mcp-server", "version": "1.0.0", "description": "A comprehensive MCP server implementation", "capabilities": { "tools": 4, "resources": 3, "prompts": 2 } } return json.dumps(info, indent=2) elif uri == "resource://data-store": return json.dumps(self.data_store, indent=2) elif uri == "resource://welcome": return """Welcome to the Complete MCP Server!
What’s Happening:
- The
@self.server.read_resource()decorator registers the read handler - Each resource URI returns appropriate content
- JSON resources use
json.dumps()to serialize data - Plain text resources return strings directly
- When a client reads a resource, this handler processes the request and returns the content
- This makes the resources functional and usable by clients
Hands-On Exercise:¶
- Implement the handler logic for the
server-authorresource you added earlier. - The resource should return your name as plain text.
- Test it later when we run the server.
- Hint: Follow the structure of the other resource handlers.
- After adding, your
read_resourcemethod should include the newserver-authorresource logic. - This exercise helps you understand how to implement resource functionality in the MCP server.
- Try to implement it on your own before looking at the solution below!
Solution for Server Author Resource Handler
Handle Errors¶
- Add Error Handling for Unknown Resources
-
Add this at the end of the
read_resourcemethod to handle unknown resources:
Skeleton 08: Register Prompts¶
Method register_prompts¶
Capabilities:
- Registers prompt templates for clients
- Defines structured prompts with parameters
- Enables prompt discovery
- Enables clients to get ready-to-use prompts
- Connects prompt templates to actual content
- Provides reusable prompt patterns
- Facilitates advanced AI interactions
- Supports dynamic prompt generation
Why This Runs Sixth:
- Defines the behavior of each prompt
- Handlers make prompts functional
- After tools and resources, prompts add higher-level interaction patterns
- Prompts build on available tools and resources
- Provides templates for AI assistants
What is a Prompt?
- A prompt is a template that guides AI assistants on how to use the server’s tools and resources effectively.
- Prompts have names, descriptions, and parameters.
- Clients can discover and request prompts.
- Examples: code review prompt, data analysis prompt
- Prompts enhance the server’s capabilities by providing structured interaction patterns.
-
Add this method to your class:
def register_prompts(self): """Register prompt templates for clients.""" @self.server.list_prompts() async def list_prompts() -> list[Prompt]: """ Return the list of available prompts. This is called when clients want to discover what prompts are available. """ return [ Prompt( name="analyze-data", description="Analyze data stored in the server", arguments=[ { "name": "key", "description": "The key of the data to analyze", "required": True } ] ), Prompt( name="calculate-scenario", description="Walk through a calculation scenario", arguments=[ { "name": "operation", "description": "The operation to demonstrate (add, subtract, multiply, divide)", "required": True } ] ) ] print("Prompts registered: analyze-data, calculate-scenario")
What’s Happening:
- The
@self.server.list_prompts()decorator registers the prompt listing handler - Each
Promptdefines a name, description, and arguments - Arguments specify what parameters the prompt template needs
- When a client calls
list_prompts(), they get this list - This makes the prompts discoverable and usable by clients
- Prompts provide structured templates for AI interactions
Hands-On Exercise:¶
- Add a new prompt called
greet-userthat prompts the AI to greet a user by name. - Define its name, description, and arguments similar to the other prompts.
- Test it later when we implement prompt handlers.
- Hint: Use the existing prompts as a reference for structure.
- After adding, your
list_promptsmethod should include the newgreet-userprompt. - This exercise helps you understand how to define and register new prompts in the MCP server.
- Try to implement it on your own before looking at the solution below!
Solution for Greet User Prompt
Skeleton 09: Prompt Handlers¶
Method: register_prompt_handlers¶
Capabilities:
- Implements prompt generation logic
- Returns formatted prompts with embedded context
- Handles prompt parameters and customization
- Provides dynamic prompt content
- Enables clients to get ready-to-use prompts
- Connects prompt templates to actual content
Why This Runs Seventh:
- After prompts are registered, we implement the logic that generates prompt content
- Prompts need implementation to generate actual text
- Connects prompt templates to actual content
- Defines the behavior of each prompt
- Handlers make prompts functional
-
Add this method to your class:
def register_prompt_handlers(self): """Implement prompt generation logic.""" @self.server.get_prompt() async def get_prompt(name: str, arguments: dict) -> list[TextContent]: """ Handle prompt generation requests. This is called when a client wants to get a prompt. """ if name == "analyze-data": key = arguments.get("key", "unknown") value = self.data_store.get(key, "not found") prompt_text = f"""Analyze the following data from the server: Key: {key} Value: {value} Please provide: 1. A description of what this data represents 2. Any patterns or insights you notice 3. Suggestions for how this data could be used Use the retrieve_data tool if you need to fetch additional context.""" return [TextContent(type="text", text=prompt_text)] elif name == "calculate-scenario": operation = arguments.get("operation", "add") prompt_text = f"""Let's work through a {operation} calculation scenario. Use the calculate tool with the operation '{operation}'. For example: - Choose two numbers (a and b) - Execute: calculate(operation="{operation}", a=10, b=5) - Explain the result This demonstrates how to use computational tools in the MCP server.""" return [TextContent(type="text", text=prompt_text)] else: return [TextContent( type="text", text=f"Error: Unknown prompt '{name}'" )] print("Prompt handlers implemented")
What’s Happening:
- The
@self.server.get_prompt()decorator registers the prompt generation handler - Each prompt returns formatted text based on the parameters
- Prompts can reference tools and resources
- Dynamic content is generated based on current server state
Hands On:¶
- Implement the handler logic for the
greet-userprompt you added earlier. - The prompt should return a greeting message using the provided
nameparameter. - Test it later when we run the server.
- Hint: Follow the structure of the other prompt handlers.
- After adding, your
get_promptmethod should include the newgreet-userprompt logic. - This exercise helps you understand how to implement prompt functionality in the MCP server.
- Try to implement it on your own before looking at the solution below!
Solution for Greet User Prompt Handler
Skeleton 10: Lifecycle Handlers¶
Method: setup_lifecycle_handlers¶
Capabilities:
- Handles server initialization events
- Manages server shutdown procedures
- Logs server lifecycle events
- Ensures clean startup and teardown
- Enables clients to get ready-to-use prompts
- Connects prompt templates to actual content
- Maintains server stability and reliability
Why This Runs Eighth:
- After all features are configured, we set up lifecycle management
- Lifecycle handlers need complete server setup
- Prepares server for actual runtime operations
- Defines the behavior of each prompt
- Handlers make prompts functional
- Ensures proper resource management during startup/shutdown
- Critical for long-running server processes
- Helps prevent resource leaks and data corruption
Why Servers Need Shutdown Procedures:
- Release system resources (memory, file handles, connections)
- Save any pending data or state to disk
- Close network connections gracefully
- Notify connected clients of server shutdown
- Clean up temporary files and caches
- Log final statistics and status
- Prevent data corruption from abrupt termination
- Allow pending operations to complete
-
Add this method to your class:
Note
- Note: MCP servers typically don’t have explicit lifecycle hooks
- This is a conceptual method showing where such logic would go
Tip
- You can implement custom startup/shutdown logic here if needed
- Use this as a placeholder for lifecycle management
- The actual “lifecycle” of the server is managed implicitly by:
- Startup: When
asyncio.run(main())is called andserver.run()begins the event loop. - Shutdown: When the process receives a signal (like
KeyboardInterrupt / Ctrl+C), which is caught in the if__name__ == "__main__":block to exit gracefully.
What’s Happening:
- MCP servers use the standard Python lifecycle
- Cleanup happens when the server process exits
- You can use
try/except/finallyblocks in the main function for cleanup - This method is a placeholder for lifecycle logic
- In real-world servers, you might add logging or resource management here
- This prepares the server for stable operation
- Enhances reliability during startup and shutdown
- Critical for production-grade servers
- Though MCP lacks explicit lifecycle hooks, this method indicates where such logic would be placed
- It serves as a reminder to consider lifecycle management in server design
- Helps maintain server health over long runtimes
- Prepares for future enhancements that may introduce lifecycle events
- Ensures the server is robust and reliable
- Maintains server integrity during its lifecycle
Skeleton 11: Run the Server¶
Method: run¶
- This is the final method to add to your class.
- It starts the MCP server and begins listening for client requests.
Capabilities:
- Starts the MCP server
- Connects to stdio transport
- Begins listening for client requests
- Runs the main event loop
- Enables clients to get ready-to-use prompts
- Connects prompt templates to actual content
- Facilitates real-time client-server communication
Why This Runs Last:
- All tools, resources, and prompts must be registered first
- This starts the actual server operation
- After this, the server is live and accepting requests
- Defines the behavior of each prompt
- Handlers make prompts functional
- This is the final step to make the server operational
- Without this, the server would not run
- This method initiates the event loop that processes requests
- Critical for real-time interactions with clients
How does MCP Server Start:¶
-
Create stdio transport:
stdio_server()- Opens stdin (standard input) for receiving messages
- Opens stdout (standard output) for sending responses
-
Run server with streams:
server.run(read_stream, write_stream)- Listens on stdin for JSON-RPC messages from client
- Sends JSON-RPC responses back on stdout
-
Event loop processes requests asynchronously
- Handles multiple concurrent requests
- Executes tools, returns resources, generates prompts
What is STDIO (Standard Input/Output)?¶
| Component | Description |
|---|---|
| stdio | Standard Input/Output streams |
| stdin | Channel for receiving data (keyboard, pipe) |
| stdout | Channel for sending data (screen, pipe) |
| Usage | MCP uses stdio for client-server communication |
| Flow | Client stdin → Server stdout → Client |
Alternatives to STDIO:¶
| Component | Description |
|---|---|
| HTTP/HTTPS | Web-based API (REST or GraphQL) |
| WebSockets | Bidirectional real-time communication |
| gRPC | High-performance RPC framework |
| Unix Domain Sockets | Local inter-process communication |
| TCP/IP Sockets | Network communication |
Why STDIO for MCP?
- ✓ Simple: No network configuration needed
- ✓ Secure: Stays within local process boundary
- ✓ Universal: Works on all operating systems
- ✓ Easy to integrate: Pipe to any process
- ✓ Lightweight: Minimal overhead for communication
- ✓ Ideal for local AI assistant integrations
- ✓ Fits well with command-line tools and scripts
- ✓ Perfect for development and testing
- ✓ Common in LSP (Language Server Protocol) implementations
- ✓ Easy to debug: View raw messages in terminal
- ✓ No firewall or network issues
- ✓ Works well with containerized environments
Async Event Loop Explained:
- Event Loop: Central coordinator for async operations
- Async/Await: Write concurrent code that looks sequential
- Non-blocking: Server handles multiple requests simultaneously
- Efficient: Uses single thread for many connections
- Scalable: Easily handles growing workloads
How It Works:
- Event loop starts and waits for events (
messages) - When message arrives, creates a Task to handle it
- While waiting for I/O (
tool execution), processes other tasks - When task completes, sends response back to client
- Continues looping until server shuts down
- This allows high concurrency with minimal threads
Benefits:
- ✓ Handle 1000s of connections with single thread
- ✓ No waiting: Process other requests during I/O
- ✓ Memory efficient: No thread per connection
- ✓ Scalable: Add more tasks without more threads
- ✓ Responsive: Quick handling of many clients
- ✓ Ideal for I/O-bound workloads (like MCP servers)
- ✓ Simplifies concurrency model
- ✓ Reduces complexity of multi-threaded code
- ✓ Easier to maintain and debug
- ✓ Leverages Python’s async capabilities effectively
-
Add this method to your class:
async def run(self): """Start the MCP server and begin serving requests.""" print("Starting MCP server...") print("Server is now running and ready to accept connections") async with stdio_server() as (read_stream, write_stream): await self.server.run( read_stream, write_stream, self.server.create_initialization_options() )
What’s Happening:
stdio_server()creates the stdin/stdout transportself.server.run()starts the server event loop- The server now listens for JSON-RPC messages on stdin
- Responses are sent back on stdout
- The server can now handle tool calls, resource reads, and prompt requests
- This is the final step to make the server operational
- The server runs indefinitely until interrupted
- Clients can now connect and interact with the server
- This method is asynchronous, allowing concurrent request handling
- The server is now live and ready for use
Skeleton 12: RAG¶
What is RAG?
- RAG = Retrieval Augmented Generation
- A technique that enhances AI responses by retrieving relevant information from a knowledge base before generating answers
- Think of it like giving the AI access to a reference library
- Combines information retrieval with text generation
- Enables accurate, context-aware responses based on specific data
- Examples: Customer support bots, domain-specific Q&A systems, documentation assistants
- Critical for providing factually accurate responses from your own data sources
Why Add RAG to Your MCP Server?
- Makes your server more intelligent and context-aware
- Allows retrieval of relevant information from local data sources
- Provides accurate responses based on your specific domain knowledge
- Enables filtering and querying of structured data
- Enhances the server’s ability to answer domain-specific questions
- No heavy vector database dependencies required for simple implementations
1. Install Dependencies¶
- No heavy dependencies (like vector databases) are required for this simple implementation.
- We will use standard Python libraries.
2. Update the MCP Server¶
- Open your MCP server file and add the following imports and initialization code to set up a simple in-memory users.
- We will also add a helper function to load data from a CSV file.
import csv
# Initialize an in-memory users
users = []
def load_users(csv_file_path: str):
"""Load users from a CSV file."""
global users
users = []
try:
with open(csv_file_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for i, row in enumerate(reader):
# Load all fields from the CSV
first_name = row.get('first_name', '')
last_name = row.get('last_name', '')
age = row.get('age', '')
city = row.get('city', '')
# Create full name and content
full_name = f"{first_name} {last_name}".strip()
content = f"{full_name} from {city}" if city else full_name
# Create user dict with all available fields
user = {
"id": str(i + 1),
"first_name": first_name,
"last_name": last_name,
"full_name": full_name,
"content": content,
"age": age,
"city": city
}
users.append(user)
print(f"Loaded {len(users)} users from CSV.")
except Exception as e:
print(f"Error loading users: {e}")
# Load the users
# Make sure you have a 'users.csv' file in the same directory
# Format: first_name,last_name,city,age
load_users("users.csv")
3. Register the RAG Tool(s)¶
- Add new tools to your MCP server that allow the agent to query this collection using simple keyword matching.
- Here are two example tools: one to filter users by city and another to filter users by age.
- Add these tool definitions to the list in your
register_toolsmethod (inside thelist_toolsreturn array):
Tool(
name="filter_users_by_city",
description="Filter and return users who live in a specific city",
inputSchema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city to filter users by"
}
},
"required": ["city"]
}
),
Tool(
name="filter_users_by_age",
description="Filter and return users who are older than the specified minimum age",
inputSchema={
"type": "object",
"properties": {
"min_age": {
"type": "number",
"description": "The minimum age to filter users by"
}
},
"required": ["min_age"]
}
)
- Then add the corresponding handlers in your
register_tool_handlersmethod (inside thecall_toolfunction):
elif name == "filter_users_by_city":
city = arguments.get("city", "")
filtered_users = []
target_city = city.lower().strip()
for user in users:
# Get city from user dict
u_city = user.get("city", "").lower().strip()
if u_city == target_city:
full_name = user.get('full_name', 'Unknown')
age = user.get('age', 'N/A')
filtered_users.append(f"User {user.get('id')}: {full_name}, Age: {age}, City: {user.get('city')}")
if not filtered_users:
result = f"No users found in {city}."
else:
result = "\n".join(filtered_users)
return [TextContent(type="text", text=result)]
elif name == "filter_users_by_age":
min_age = int(arguments.get("min_age", 0))
filtered_users = []
for user in users:
# Get age from user dict
u_age = user.get("age", "")
# Skip if no age
if not u_age:
continue
try:
u_age = int(u_age)
except (ValueError, TypeError):
continue
if u_age > min_age:
full_name = user.get('full_name', 'Unknown')
city = user.get('city', 'Unknown')
filtered_users.append(f"User {user.get('id')}: {full_name}, Age: {u_age}, City: {city}")
if not filtered_users:
result = f"No users found older than {min_age}."
else:
result = "\n".join(filtered_users)
return [TextContent(type="text", text=result)]
4. Test the RAG Capabilities¶
- Restart your MCP server.
- Use the MCP Inspector or Client to call
query_userswith a question like “What is a Pod?”. - Verify that the tool returns the specific definition we added to the database.
Skeleton 13: Roots¶
Skeleton 14: Main / Entry Point¶
What is the Main Entry Point?
- The main entry point is the starting point of your Python script
- It’s the function that orchestrates the entire server setup and execution
- Think of it like the conductor of an orchestra - it coordinates all the pieces
- The
main()function calls all setup methods in the correct order - The
if __name__ == "__main__"block is what runs when you execute the script directly - Examples: Starts server, initializes components, handles graceful shutdown
- Essential for any Python application that needs to run as a standalone program
Why This is Important:
- Ensures all components are initialized in the correct order
- Provides a clear execution flow that’s easy to understand
- Handles errors and graceful shutdown (like Ctrl+C)
- Makes your code modular and testable
- Standard Python pattern for executable scripts
- Without this, your server would just be a collection of classes with no way to run
Orchestration Order:
- Create server instance (constructor)
- Register tools
- Register tool handlers
- Register resources
- Register resource handlers
- Register prompts
- Register prompt handlers
- Setup lifecycle handlers
- Run the server
- Now we need to create the main function that orchestrates everything and the entry point that runs when the script is executed.
- This is where we call all the setup methods in order and start the server.
- This is the final piece to complete your MCP server implementation.
- Let’s add the main function and entry point.
-
Add these functions at the end of your file (outside the class):
async def main(): """ Main entry point for the MCP server. This function orchestrates the complete server setup and execution: 1. Creates server instance (constructor) 2. Registers tools 3. Registers tool handlers 4. Registers resources 5. Registers resource handlers 6. Registers prompts 7. Registers prompt handlers 8. Sets up lifecycle handlers 9. Runs the server """ print("="*80) print("🌟 COMPLETE MCP SERVER - STARTING") print("="*80) # Step 1: Create server instance server = CompleteMCPServer() # Step 2: Register tools server.register_tools() # Step 3: Register tool handlers server.register_tool_handlers() # Step 4: Register resources server.register_resources() # Step 5: Register resource handlers server.register_resource_handlers() # Step 6: Register prompts server.register_prompts() # Step 7: Register prompt handlers server.register_prompt_handlers() # Step 8: Setup lifecycle handlers server.setup_lifecycle_handlers() print("="*80) print("All components registered successfully!") print("="*80) # Step 9: Run the server await server.run() if __name__ == "__main__": """ Entry point when script is run directly. This runs when you execute: python mcp_server.py """ try: asyncio.run(main()) except KeyboardInterrupt: print("\n👋 Server shutdown complete") sys.exit(0)
What’s Happening:
- The
main()function calls all setup methods in order - The
if __name__ == "__main__"block runs when the script is executed asyncio.run(main())starts the async event loopKeyboardInterrupthandler allows graceful shutdown with Ctrl+C- This is the final orchestration of the MCP server
Code Review¶
At this point, your mcp_server.py file should have:
- All imports at the top
CompleteMCPServerclass with all 9 methodsmain()function- Entry point with
if __name__ == "__main__"
Testing with MCP Inspector¶
- Now that you’ve built your complete MCP server, it’s time to test it!
- We’ll use MCP Inspector, a web-based tool for debugging MCP servers.
- Follow the steps below to install MCP Inspector, run your server, and test all the tools, resources, and prompts you implemented.
What is MCP Inspector?¶
MCP Inspector is a web-based debugging tool for MCP servers. Think of it like a browser developer console for your MCP server - it lets you:
- Connect to your server
- See all available tools, resources, and prompts
- Execute tools with custom parameters
- Read resources
- Generate prompts
- View JSON-RPC messages
- Debug server behavior
Installing MCP Inspector¶
Open a terminal and run:
# Install MCP Inspector globally
npm install -g @modelcontextprotocol/inspector
# Run the MCP Inspector
npx @modelcontextprotocol/inspector python3 "mcp_server.py"
Testing Your Tools¶
Follow these steps in the MCP Inspector:
Test 01: Connect to the Server¶
- Click the “Connect” button at the bottom left of the interface
- Wait for the connection status to show “Connected” (green indicator)
- If not connected, set the following:
- transport:
stdio - Command:
python3 - Arguments:
mcp_server.py - Click “Connect” again
- You should see the server name and version in the top right corner
- Success!
Test 02: Explore Tools¶
- Click the “Tools” tab in the upper menu
- Click “List tools” to see all available tools
- You should see:
calculate,store_data,retrieve_data,echo+ RAG tools if added - If you added the
greetingtool, you should see that too!
Test 03: Test the Calculate Tool¶
- Click on “calculate” in the tools list
- The tool interface opens on the right side
- Fill in the parameters:
- operation: Select “add” from the dropdown
- a: Enter
10 - b: Enter
5
- a: Enter
- operation: Select “add” from the dropdown
- Click “Run Tool”
- Scroll down to see the result:
"Result: 10 add 5 = 15" - Success!
Test 04: Test the Store Data Tool¶
- Click on “store_data” in the tools list
- Fill in the parameters:
- key: Enter
username - value: Enter
Alice
- key: Enter
- Click “Run Tool”
- Result:
"Stored: username = Alice" - Try storing another key-value pair to see it works!
- Success!
Test 05: Test the Retrieve Data Tool¶
- Click on “retrieve_data” in the tools list
- Fill in the parameter:
- key: Enter
username - Click “Run Tool”
- Result:
"Retrieved: username = Alice" - Try retrieving a non-existent key to see error handling!
Test 06: Test the Echo Tool¶
- Click on “echo” in the tools list
- Fill in the parameter:
- text: Enter
Hello, MCP World! - Click “Run Tool”
- Result:
"Echo: Hello, MCP World!"
Test 07: Test Resources¶
- Click the “Resources” tab in the upper menu
- Click “List resources” to see all available resources
- You should see:
server-info,data-store,welcome - If you added the
server-authorresource, you should see that too!
Test Resource: server-info¶
- Click on “resource://server-info”
- View the JSON response showing server metadata
- Notice it shows 4 tools, 3 resources, 2 prompts
Test Resource: data-store¶
- Click on “resource://data-store”
- View the current contents of the data store
- You should see the
username: Aliceyou stored earlier!
Test Resource: welcome¶
- Click on “resource://welcome”
- View the welcome message explaining server capabilities
- If you added the
server-authorresource, click on it to see your name displayed - Success!
Test 08: Testing Your Prompts¶
- Click the “Prompts” tab in the upper menu
- Click “List prompts” to see all available prompts
- You should see:
analyze-data,calculate-scenario
Test Prompt: calculate-scenario¶
- Click on “calculate-scenario”
- Fill in the argument:
- operation: Enter
multiply
- operation: Enter
- Click “Get Prompt”
- View the generated prompt that explains how to use the calculate tool
Test Prompt: analyze-data¶
- Click on “analyze-data”
- Fill in the argument:
- key: Enter
username
- key: Enter
- Click “Get Prompt”
- View the generated prompt that analyzes the stored data
Understanding the Inspector Interface¶
Left Panel: Navigation
- Tools, Resources, Prompts tabs
- List and select items to test
Right Panel: Details
- Shows selected item details
- Input forms for parameters
- Execute button
- Results display
Bottom Panel: JSON-RPC Messages
- Shows raw protocol messages
- Useful for debugging
- See requests and responses
Connection Status
- Top right corner
- Green = Connected
- Red = Disconnected
- Shows server name and version
Advanced Experiments¶
- Now that you have a working server, try these challenges:
Challenge 1: Modify the Calculate Tool¶
Add support for:
- power operation (a^b)
- modulus operation (a % b)
Challenge 2: Add Roots¶
Add support for: - Listing files in a directory (referencing client roots) - Reading file contents (referencing client roots)
Bonus Task: Hands-On Exercise¶
Add Pagination Support for Listing Users¶
Objective: Implement a new tool called list_all_users that returns all users with pagination support.
Requirements:
- Tool Name:
list_all_users - Parameters:
page(optional, default: 1) - The page number to retrieveper_page(optional, default: 10) - Number of users per page- Functionality:
- Return users for the specified page
- Include metadata: total users, total pages, current page
- Handle edge cases (invalid page numbers, empty results)
Your Task:
- Add the tool definition to
register_tools()method - Implement the tool handler in
register_tool_handlers()method - Test your implementation using MCP Inspector
Hints:
- Use Python’s list slicing for pagination: users[start:end]
- Calculate start index: (page - 1) * per_page
- Calculate total pages: math.ceil(len(users) / per_page)
- Return both the user list and metadata
Walkthrough Solution¶
Step 1: Add Tool Definition¶
Click here for the solution
Add this to your `register_tools()` method in the tools list:Tool(
name="list_all_users",
description="List all users with pagination support",
inputSchema={
"type": "object",
"properties": {
"page": {
"type": "number",
"description": "Page number (default: 1)",
"default": 1
},
"per_page": {
"type": "number",
"description": "Number of users per page (default: 10)",
"default": 10
}
}
}
)
Step 2: Add Tool Handler¶
Click here for the solution
Add this to your `register_tool_handlers()` method in the `call_tool` function:elif name == "list_all_users":
import math
# Get pagination parameters
page = int(arguments.get("page", 1))
per_page = int(arguments.get("per_page", 10))
# Validate parameters
if page < 1:
page = 1
if per_page < 1:
per_page = 10
if per_page > 100: # Max limit
per_page = 100
# Calculate pagination
total_users = len(users)
total_pages = math.ceil(total_users / per_page) if total_users > 0 else 1
# Ensure page doesn't exceed total pages
if page > total_pages:
page = total_pages
# Calculate slice indices
start_idx = (page - 1) * per_page
end_idx = start_idx + per_page
# Get paginated users
paginated_users = users[start_idx:end_idx]
# Format output
user_list = []
for user in paginated_users:
full_name = user.get('full_name', 'Unknown')
age = user.get('age', 'N/A')
city = user.get('city', 'Unknown')
user_list.append(f"User {user.get('id')}: {full_name}, Age: {age}, City: {city}")
# Build result with metadata
metadata = f"Page {page} of {total_pages} | Total Users: {total_users} | Showing: {len(paginated_users)}"
result = f"{metadata}\n\n" + "\n".join(user_list)
return [TextContent(type="text", text=result)]
Step 3: Test in MCP Inspector¶
- Restart your MCP server
- Open MCP Inspector
- Go to the Tools tab
- Click on “list_all_users”
- Test with different parameters:
- Default (page: 1, per_page: 10)
- Page 2 with 5 users per page
- Page 5 with 20 users per page
Expected Output Format:
Page 1 of 10 | Total Users: 100 | Showing: 10
User 1: James Smith, Age: 24, City: New York
User 2: Maria Garcia, Age: 31, City: Los Angeles
...
Congratulations! 🎉¶
You’ve successfully built a complete MCP server with: - Multiple tools (calculate, data storage, echo, user filters, pagination) - Resources (server info, data store, welcome message) - Prompts (data analysis, calculation scenarios) - RAG capabilities (user filtering and search) - Pagination support (bonus feature)
Keep exploring and building more advanced MCP servers!
Implementing Live MCP Server (With Ollama Integration)¶
Overview¶
- In this lab, you’ll master the art of creating sophisticated, production-ready
MCPtools that can handle complex inputs, perform real-world operations, and return rich content types.
Learning Objectives¶
By the end of this lab, you will:
- Design robust tool schemas with advanced validation
- Implement tools that interact with external systems (APIs, databases, file systems)
- Return multiple content types (text, images, resources)
- Handle errors gracefully with detailed feedback
- Implement async operations and streaming responses
- Apply best practices for tool composition
- Test tools thoroughly with various edge cases
Prerequisites¶
- Completion of previous MCP labs or equivalent experience
- Understanding of async/await in JavaScript/TypeScript
- Basic knowledge of REST APIs and JSON
- Node.js development environment set up
Weather Information with Ollama¶
Goal¶
- Create a production-ready weather tool that uses Ollama (local AI) to generate weather information, handles errors gracefully, and returns formatted information.
Complete Weather Tool Implementation with Ollama¶
- Here is the complete
src/index.tsfile with the Ollama-based weather tool added.
Click to expand code
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
/**
* Create an MCP server with core capabilities
*/
class MyFirstMCPServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: "my-first-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
/**
* Set up request handlers
*/
private setupHandlers(): void {
// Handler for listing available tools
this.server.setRequestHandler(
ListToolsRequestSchema,
async () => ({
tools: [
{
name: "get_weather",
description: "Get current weather information for a city using AI",
inputSchema: {
type: "object",
properties: {
city: {
type: "string",
description: "City name (e.g., 'London', 'New York')"
},
units: {
type: "string",
description: "Temperature units",
enum: ["celsius", "fahrenheit"],
default: "celsius"
}
},
required: ["city"]
}
},
{
name: "hello_world",
description: "Returns a friendly greeting message",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "The name to greet",
},
},
required: ["name"],
},
},
],
})
);
// Handler for calling tools
this.server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
const { name, arguments: args } = request.params;
if (name === "get_weather") {
try {
// Extract and validate parameters
const city = args.city as string;
const units = (args.units as string) || "celsius";
if (!city || city.trim().length === 0) {
throw new Error("City name cannot be empty");
}
// Use Ollama to generate weather information
const prompt = `Generate realistic current weather information for ${city}.
Return ONLY a JSON object with this exact structure:
{
"name": "${city}",
"sys": {"country": "XX"},
"main": {"temp": 20.5, "feels_like": 22.1, "humidity": 65},
"weather": [{"description": "clear sky"}],
"wind": {"speed": 3.2}
}
Use realistic weather data appropriate for the location. Temperature should be in Celsius. Choose an appropriate 2-letter country code for the city. Make the weather description realistic for the location and season.`;
// Call Ollama API
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-oss:20b',
prompt: prompt,
stream: false,
format: 'json'
}),
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status} ${response.statusText}. Make sure Ollama is running with 'ollama serve'.`);
}
const ollamaResult = await response.json();
let data;
try {
// Parse the JSON response from Ollama
data = JSON.parse(ollamaResult.response);
} catch (parseError) {
// Fallback to mock data if parsing fails
console.warn('Failed to parse Ollama response, using fallback data');
const fallbackData: Record<string, any> = {
"london": {
name: "London",
sys: { country: "GB" },
main: { temp: 15.2, feels_like: 14.8, humidity: 82 },
weather: [{ description: "light rain" }],
wind: { speed: 3.6 }
},
"new york": {
name: "New York",
sys: { country: "US" },
main: { temp: 22.5, feels_like: 24.1, humidity: 65 },
weather: [{ description: "clear sky" }],
wind: { speed: 2.1 }
},
"tokyo": {
name: "Tokyo",
sys: { country: "JP" },
main: { temp: 18.7, feels_like: 18.2, humidity: 78 },
weather: [{ description: "few clouds" }],
wind: { speed: 1.8 }
},
"paris": {
name: "Paris",
sys: { country: "FR" },
main: { temp: 12.8, feels_like: 11.9, humidity: 71 },
weather: [{ description: "overcast clouds" }],
wind: { speed: 4.2 }
},
"sydney": {
name: "Sydney",
sys: { country: "AU" },
main: { temp: 24.3, feels_like: 25.1, humidity: 73 },
weather: [{ description: "sunny" }],
wind: { speed: 2.8 }
}
};
data = fallbackData[city.toLowerCase().trim()] || fallbackData["london"];
}
// Format response
const tempUnit = units === "fahrenheit" ? "°F" : "°C";
const weatherText = `
Weather in ${data.name}, ${data.sys.country}:
- Temperature: ${data.main.temp}${tempUnit}
- Feels like: ${data.main.feels_like}${tempUnit}
- Conditions: ${data.weather[0].description}
- Humidity: ${data.main.humidity}%
- Wind Speed: ${data.wind.speed} m/s
*Generated by Ollama AI*
`.trim();
// Return MCP response
return {
content: [
{
type: "text",
text: weatherText
}
]
};
} catch (error) {
// Handle errors
throw new Error(
`Failed to get weather: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
if (name === "hello_world") {
const userName = args?.name as string;
if (!userName) {
throw new Error("Name parameter is required");
}
return {
content: [
{
type: "text",
text: `Hello, ${userName}! Welcome to your first MCP server! 🎉`,
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
}
);
}
/**
* Set up error handling
*/
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
/**
* Start the server
*/
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("My First MCP Server running on stdio");
}
}
/**
* Main entry point
*/
async function main() {
const server = new MyFirstMCPServer();
await server.start();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Test the Weather Tool¶
- This MCP is wriitten in TypeScript. Make sure you have all dependencies installed with
npm install.
# Install dependencies
npm install @types/node tsx typescript
# Test the MCP server
# Start the MCP Inspector** (in a new terminal):
npx @modelcontextprotocol/inspector tsx mcp.ts
-
In the MCP Inspector interface:
- You should see both
get_weatherandhello_worldtools listed - Click on
get_weathertool - Enter a city name like “London”, “New York”, “Tokyo”, “Paris”, or “Sydney”
- Optionally set units to “fahrenheit” for Fahrenheit temperatures
- Click “Call Tool”
- You should see both
-
Test different scenarios:
- Valid cities: “London”, “New York”, “Tokyo”, “Paris”, “Sydney”
- Invalid cities: “InvalidCity123” (will use fallback data)
- Different units: Try both “celsius” and “fahrenheit”
- Empty city: Try with empty string (should show validation error)
-
Test error cases:
- Stop Ollama server and try calling the tool (should show API error)
- Try with invalid model name in the code (should show error)
-
You should see AI-generated weather information formatted like this:
Tool 2: File Operations¶
Goal¶
- Create a secure file reading tool that can handle various file types, validate paths, and return formatted content with metadata.
Complete File Operations Tool Implementation¶
- Do NOT copy the entire code block below.
- Instead, add the
read_filetool to your existingsrc/index.tsfile by following these specific steps:
-
Add the import at the top of your file (after existing imports):
-
Add the
read_filetool to your tools array in theListToolsRequestSchemahandler:{ name: "read_file", description: "Read contents of a text file with security validation", inputSchema: { type: "object", properties: { filepath: { type: "string", description: "Absolute path to the file" }, encoding: { type: "string", description: "File encoding", enum: ["utf8", "ascii", "base64"], default: "utf8" }, maxSize: { type: "number", description: "Maximum file size in bytes", minimum: 1, maximum: 10485760, default: 1048576 } }, required: ["filepath"] } } -
Add the
read_filehandler in theCallToolRequestSchemahandler (before the finalthrow new Error):if (name === "read_file") { try { const filepath = args.filepath as string; const encoding = (args.encoding as BufferEncoding) || "utf8"; const maxSize = (args.maxSize as number) || 1048576; // Security: Validate input if (!filepath || typeof filepath !== 'string' || filepath.trim().length === 0) { throw new Error("filepath must be a non-empty string"); } // Security: Resolve and validate path const resolvedPath = path.resolve(filepath); // Prevent directory traversal attacks if (!resolvedPath.startsWith(process.cwd())) { throw new Error("Access denied: file path outside allowed directory"); } // Check if file exists and is readable try { await fs.access(resolvedPath, fs.constants.R_OK); } catch { throw new Error(`File not found or not readable: ${filepath}`); } // Get file stats const stats = await fs.stat(resolvedPath); // Check if it's actually a file (not a directory) if (!stats.isFile()) { throw new Error(`Path is not a file: ${filepath}`); } // Check file size if (stats.size > maxSize) { throw new Error( `File too large: ${stats.size} bytes (max: ${maxSize})` ); } // Read file content const content = await fs.readFile(resolvedPath, encoding); // Format response with metadata const fileInfo = { path: resolvedPath, size: stats.size, modified: stats.mtime.toISOString(), encoding: encoding }; return { content: [ { type: "text", text: `File Information:\n${JSON.stringify(fileInfo, null, 2)}\n\nContent:\n${content}` } ] }; } catch (error) { throw new Error( `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }
Testing the File Operations Tool¶
Step 1: Create Test File¶
# Create a test directory and files
mkdir -p test-files
echo "Hello, this is a test file\!" > test-files/hello.txt
echo '{"name": "test", "value": 123}' > test-files/data.json
echo "Line 1\nLine 2\nLine 3" > test-files/lines.txt
Step 2: Start the MCP Inspector¶
Step 3: Test File Reading¶
-
Test with a simple text file:
- Tool:
read_file - filepath:
/absolute/path/to/test-files/hello.txt(use the full absolute path) - Click “Call Tool”
- Tool:
-
Test with JSON file:
- Tool:
read_file - filepath:
/absolute/path/to/test-files/data.json - Click “Call Tool”
- Tool:
-
Test with different encoding:
- Tool:
read_file - filepath:
/absolute/path/to/test-files/hello.txt - encoding:
base64 - Click “Call Tool”
- Tool:
-
Test file size limit:
- Create a large file:
dd if=/dev/zero of=test-files/large.txt bs=1M count=2 - Try reading it with default maxSize (1MB)
- Try with maxSize:
2097152(2MB)
- Create a large file:
Step 4: Test Error Cases¶
-
Non-existent file:
- filepath:
/absolute/path/to/test-files/nonexistent.txt
- filepath:
-
Directory instead of file:
- filepath:
/absolute/path/to/test-files(the directory itself)
- filepath:
-
Empty filepath:
- filepath:
""
- filepath:
-
Path traversal attempt:
- filepath:
/absolute/path/../../../etc/passwd
- filepath:
Step 5: Verify Output¶
-
You should see output like:
Troubleshooting:¶
- “File not found”: Make sure you’re using the absolute path
- “Access denied”: The file path is outside your project directory
- “Path is not a file”: You tried to read a directory
- “File too large”: Increase the maxSize parameter
Key Learning Points:¶
- Path security and preventing directory traversal attacks
- File system operations with Node.js fs/promises
- Input validation beyond JSON Schema
- File metadata extraction and formatting
- Error handling for various file system scenarios
- Resource limits to prevent abuse
Tool 3: Database Query¶
Goal¶
- Create a secure database query tool that can execute
SELECTstatements on aSQLitedatabase with proper validation and safety measures.
Complete Database Query Tool Implementation¶
-
First, install the SQLite dependency:
-
Now add the
query_databasetool to your existingsrc/index.tsfile by following these specific steps: -
Add the imports at the top of your file (after existing imports):
-
Add the
query_databasetool to your tools array in theListToolsRequestSchemahandler:{ name: "query_database", description: "Execute SELECT queries on a SQLite database", inputSchema: { type: "object", properties: { query: { type: "string", description: "SQL SELECT query to execute" }, parameters: { type: "array", description: "Query parameters for prepared statement", items: { type: ["string", "number", "boolean", "null"] }, default: [] }, limit: { type: "number", description: "Maximum number of rows to return", minimum: 1, maximum: 1000, default: 100 } }, required: ["query"] } } -
Add the
query_databasehandler in theCallToolRequestSchemahandler (before the finalthrow new Error):if (name === "query_database") { try { const query = args.query as string; const parameters = (args.parameters as any[]) || []; const limit = (args.limit as number) || 100; // Security: Validate input if (!query || typeof query !== 'string' || query.trim().length === 0) { throw new Error("query must be a non-empty string"); } // Security: Only allow SELECT queries const trimmedQuery = query.trim().toUpperCase(); if (!trimmedQuery.startsWith('SELECT')) { throw new Error("Only SELECT queries are allowed for security"); } // Check if database file exists const dbPath = './data.db'; try { await fs.access(dbPath, fs.constants.R_OK); } catch { throw new Error("Database file 'data.db' not found in project root"); } // Open database in read-only mode const db = new Database(dbPath, { readonly: true }); try { // Prepare statement const stmt = db.prepare(query + ' LIMIT ?'); // Execute query const rows = stmt.all(...parameters, limit); // Format results const resultText = rows.length > 0 ? JSON.stringify(rows, null, 2) : "No results found"; // Get query info const info = stmt.columns(); const columnNames = info.map(col => col.name); return { content: [ { type: "text", text: `Query executed successfully.\nDatabase: ${dbPath}\nColumns: ${columnNames.join(', ')}\nRows returned: ${rows.length}\n\nResults:\n${resultText}` } ] }; } finally { db.close(); } } catch (error) { throw new Error( `Database query failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }
Testing the Database Query Tool¶
Step 1: Install SQLite
# Install sqlite3 command-line tool (if not already installed)
# On macOS:
brew install sqlite3
# On Linux (Ubuntu/Debian):
sudo apt-get update && sudo apt-get install sqlite3
# On Linux (CentOS/RHEL/Fedora):
sudo yum install sqlite3 # or sudo dnf install sqlite3
# On Windows (using Chocolatey):
choco install sqlite
# On Windows (manual download):
# Download from: https://www.sqlite.org/download.html
# Extract sqlite3.exe to a folder in your PATH
# Verify installation:
sqlite3 --version
Step 2: Create a Sample Database
Navigate to your MCP server directory
Run the database creation command
sqlite3 data.db << 'EOF'
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL,
category TEXT,
in_stock BOOLEAN DEFAULT 1
);
INSERT INTO users (name, email, age) VALUES
('Alice Johnson', 'alice@example.com', 28),
('Bob Smith', 'bob@example.com', 34),
('Charlie Brown', 'charlie@example.com', 22);
INSERT INTO products (name, price, category, in_stock) VALUES
('Laptop', 999.99, 'Electronics', 1),
('Book', 19.99, 'Education', 1),
('Coffee Mug', 12.50, 'Kitchen', 0);
.quit
EOF
Verify the database was created
-
You should see output like:
What this does:
- Creates a SQLite database file called
data.dbin your project directory - Creates two tables:
usersandproducts - Inserts sample data into both tables
- This gives you test data to query with your
query_databasetool
Step 2: Start the MCP Inspector
Step 3: Test Database Queries
-
Simple SELECT query:
- Tool:
query_database - query:
SELECT * FROM users - Click “Call Tool”
- Tool:
-
Query with WHERE clause:
- Tool:
query_database - query:
SELECT name, email FROM users WHERE age > 25 - Click “Call Tool”
- Tool:
-
Query with parameters:
- Tool:
query_database - query:
SELECT * FROM products WHERE category = ? - parameters:
["Electronics"] - Click “Call Tool”
- Tool:
-
Query with LIMIT:
- Tool:
query_database - query:
SELECT * FROM users - limit:
2 - Click “Call Tool”
- Tool:
-
JOIN query:
- Tool:
query_database - query:
SELECT u.name, p.name as product FROM users u CROSS JOIN products p LIMIT 5 - Click “Call Tool”
- Tool:
Step 4: Test Error Cases
-
Non-SELECT query:
- query:
DELETE FROM users WHERE id = 1
- query:
-
Invalid SQL syntax:
- query:
SELECT * FROM nonexistent_table
- query:
-
Missing database file:
- Rename
data.dbtodata.db.backupand try a query
- Rename
-
Empty query:
- query:
""
- query:
Step 5: Verify Output
-
You should see output like:
Troubleshooting:
- “Database file not found”: Make sure
data.dbexists in your project root - “Only SELECT queries are allowed”: The tool only allows SELECT statements for security
- “no such table”: Check your table names in the database
- “sqlite3: command not found”: Install sqlite3 CLI tool
Key Learning Points:¶
- SQL injection prevention using prepared statements
- Database security with read-only access and query restrictions
- SQLite operations with better-sqlite3
- Query parameterization for safe dynamic queries
- Result formatting and metadata extraction
- Resource management with proper database connection handling
Returning Rich Content¶
- MCP supports multiple content types in tool responses, allowing you to return not just text but also images, resources, and combinations of different content types.
- This enables richer, more interactive responses that can include visual data, file references, and structured information.
1. Text Content¶
Text content is the most common and basic type of response.
Use it for any string-based information like analysis results, status messages, or formatted data.
When to use - Most tool responses will use text content. It’s perfect for:
- Status messages and confirmations
- Formatted data output (JSON, tables, lists)
- Error messages and explanations
- Analysis results and summaries
2. Image Content¶
Image content allows you to return visual data directly in the response. The image data must be base64-encoded and include the appropriate MIME type.
When to use - Ideal for tools that generate or process visual content:
- Charts and graphs from data analysis
- Screenshots or visual captures
- Generated diagrams or illustrations
- Image processing results
Important: Always specify the correct MIME type (image/png, image/jpeg, image/svg+xml, etc.) and ensure the base64 data is properly encoded.
3. Resource Content¶
Resource content references external resources rather than including their data directly. This is useful for large files or when you want to provide access to resources without embedding them.
return {
content: [
{
type: "resource",
resource: {
uri: "file:///path/to/file.txt",
mimeType: "text/plain",
text: "File contents..."
}
}
]
};
When to use - Best for:
- Large files that would make responses too bulky
- References to external files or URLs
- When the client should handle the resource directly
- Providing access to generated files
Note: The text field is optional - you can omit it if the resource content is too large or if you just want to provide a reference.
4. Multiple Content Items¶
Multiple content items allow you to combine different types of content in a single response. This creates rich, multi-part responses that can include text explanations alongside visual data.
return {
content: [
{
type: "text",
text: "Analysis complete:"
},
{
type: "text",
text: "Details:\n- Item 1\n- Item 2"
},
{
type: "image",
data: chartImage,
mimeType: "image/png"
}
]
};
When to use - Perfect for comprehensive responses that need multiple components:
- Analysis reports with both text summaries and visual charts
- File processing results with metadata and content preview
- Multi-step operations with status updates and final results
- Complex data with both tabular and graphical representations
Tip: Order your content logically - start with text explanations, then show supporting images or resources.
Error Handling Patterns¶
Error handling is crucial for robust MCP tools. Different situations require different approaches to handle failures gracefully while providing useful feedback to users. Here are three essential patterns for handling errors effectively.
Pattern 1: Input Validation¶
Input validation ensures that tool arguments meet your requirements before processing begins. This prevents runtime errors and provides clear feedback when users provide invalid data.
function validateInput(args: any): void {
if (!args.filepath || typeof args.filepath !== 'string') {
throw new Error("filepath must be a non-empty string");
}
if (args.maxSize && (args.maxSize < 1 || args.maxSize > 10485760)) {
throw new Error("maxSize must be between 1 and 10485760 bytes");
}
}
When to use - Always validate inputs before processing, even when using JSON Schema validation. This pattern is essential for:
- Type checking beyond JSON Schema capabilities
- Business logic validation (file size limits, path security)
- Preventing runtime errors from malformed data
- Providing specific, actionable error messages
Why it matters: Early validation fails fast and gives users clear guidance on how to fix their input.
Pattern 2: Graceful Degradation¶
Graceful degradation provides partial functionality when full operation isn’t possible. Instead of failing completely, the tool returns useful information or falls back to alternative approaches.
try {
const data = await fetchFromAPI(url);
return formatSuccess(data);
} catch (error) {
// Log error but return partial results if possible
console.error("API call failed:", error);
return {
content: [
{
type: "text",
text: "⚠️ Could not fetch live data. Using cached results..."
}
]
};
}
When to use - For external dependencies that might be unreliable:
- API calls that could timeout or fail
- Network-dependent operations
- Services with occasional downtime
- When partial results are better than no results
Why it matters: Users get some value even when systems are partially broken, improving overall reliability and user experience.
Pattern 3: Detailed Error Context¶
Detailed error context provides comprehensive information for debugging while keeping user-facing messages clean. Log full details internally but expose only safe, helpful information to users.
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const errorContext = {
tool: name,
arguments: args,
timestamp: new Date().toISOString(),
error: errorMessage
};
console.error("[Tool Error]", JSON.stringify(errorContext));
throw new Error(
`Tool '${name}' failed: ${errorMessage}. Check server logs for details.`
);
}
When to use - For complex operations where debugging might be needed:
- Multi-step processes with potential failure points
- Operations involving external systems
- When you need to track error patterns over time
- Production environments where detailed logging is crucial
Why it matters: Developers can diagnose issues effectively while users get clear, non-technical error messages.
Best Practices for Error Handling:¶
- Fail Fast: Validate inputs early and stop processing on critical errors
- Log Internally: Use
console.error()for detailed logging (goes to stderr, not stdout) - User-Friendly Messages: Keep error messages clear and actionable
- Don’t Leak Sensitive Data: Never expose file paths, credentials, or internal details
- Consistent Format: Use similar error message patterns across tools
- Recovery Options: When possible, suggest how users can resolve the issue
Async Operations and Performance¶
MCP tools often need to handle asynchronous operations and optimize performance. Long-running tasks require special handling to prevent timeouts and provide feedback, while expensive operations benefit from caching to improve response times and reduce resource usage.
Long-Running Operations¶
Long-running operations need monitoring and progress feedback to prevent timeouts and keep users informed. Use logging and timing to track operation progress and provide completion status.
if (name === "analyze_large_file") {
const filepath = args.filepath as string;
// For very long operations, consider streaming or progress updates
console.error(`[INFO] Starting analysis of ${filepath}...`);
try {
const startTime = Date.now();
// Perform analysis
const result = await performLongAnalysis(filepath);
const duration = Date.now() - startTime;
console.error(`[INFO] Analysis completed in ${duration}ms`);
return {
content: [
{
type: "text",
text: `Analysis Results (completed in ${duration}ms):\n\n${result}`
}
]
};
} catch (error) {
console.error(`[ERROR] Analysis failed after ${Date.now() - startTime}ms`);
throw error;
}
}
When to use - For operations that take more than a few seconds:
- Large file processing or analysis
- Complex computations
- External API calls with potential delays
- Batch operations on multiple items
Why it matters: Prevents timeouts, provides user feedback, enables monitoring and debugging of slow operations.
Caching Results¶
Caching results stores expensive operation results to avoid redundant computation. Use time-based expiration and proper cache keys for efficient reuse of results.
class CachedMCPServer {
private cache: Map<string, { data: any; timestamp: number }>;
private cacheTTL: number = 60000; // 1 minute
constructor() {
this.cache = new Map();
}
private getCacheKey(toolName: string, args: any): string {
return `${toolName}:${JSON.stringify(args)}`;
}
private getCached(key: string): any | null {
const cached = this.cache.get(key);
if (!cached) return null;
if (Date.now() - cached.timestamp > this.cacheTTL) {
this.cache.delete(key);
return null;
}
return cached.data;
}
private setCache(key: string, data: any): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
}
When to use - For expensive operations that return consistent results:
- API calls to external services
- Complex calculations or data processing
- Database queries with static data
- File analysis that doesn’t change frequently
Why it matters: Dramatically improves response times, reduces resource usage, and provides better user experience for repeated requests.
Best Practices for Async Operations and Performance:¶
- Monitor Execution Time: Log start/end times for operations over 1 second
- Set Reasonable Timeouts: Use appropriate timeouts for external calls (5-30 seconds)
- Cache Strategically: Cache expensive operations but consider data freshness
- Use Streaming: For very large responses, consider streaming or pagination
- Resource Cleanup: Always clean up connections, file handles, and memory
- Progress Feedback: For long operations, provide progress updates via logging
- Memory Management: Be mindful of memory usage in long-running processes
Tool Composition¶
Tool composition is the art of designing MCP tools that work seamlessly together, allowing LLMs to chain multiple tools to accomplish complex tasks. Well-composed tools create a powerful ecosystem where each tool handles a specific responsibility while enabling sophisticated workflows through intelligent combination.
Example: Multi-Step Analysis¶
Multi-step analysis demonstrates how simple, focused tools can be combined to perform complex data processing workflows. Each tool has a clear responsibility and can be used independently or as part of larger operations.
// Tool 1: List files
{
name: "list_files",
description: "List files in a directory",
inputSchema: { ... }
}
// Tool 2: Read file
{
name: "read_file",
description: "Read a specific file",
inputSchema: { ... }
}
// Tool 3: Analyze content
{
name: "analyze_text",
description: "Analyze text content",
inputSchema: { ... }
}
When to use - For workflows that require multiple processing steps:
- Data analysis pipelines
- File processing workflows
- Multi-stage computations
- Complex research tasks
Why it matters: Breaks down complex problems into manageable, reusable components that can be combined in flexible ways.
LLM Tool Chaining¶
LLM tool chaining allows AI models to automatically sequence tool calls based on intermediate results. The LLM analyzes outputs from one tool and determines which tool to call next, creating intelligent workflows without explicit programming.
The LLM can chain these tools:
- List files in directory - Discover available files
- Read interesting files - Access content based on filenames
- Analyze their content - Process and extract insights
When to use - When tasks naturally break down into sequential steps:
- Research and analysis workflows
- Data processing pipelines
- Content generation chains
- Problem-solving sequences
Why it matters: Enables complex, multi-step reasoning and problem-solving that would be difficult to implement in single tools.
Testing Composed Tools¶
Testing composed tools ensures that individual tools work correctly both in isolation and when chained together. Use comprehensive test suites that cover single-tool usage and multi-tool workflows.
import { describe, it, expect } from 'vitest';
describe('Weather Tool', () => {
it('should validate city name', async () => {
await expect(
callTool('get_weather', { city: '' })
).rejects.toThrow('City name cannot be empty');
});
it('should handle invalid city', async () => {
await expect(
callTool('get_weather', { city: 'InvalidCity12345' })
).rejects.toThrow('not found');
});
it('should return weather data', async () => {
const result = await callTool('get_weather', {
city: 'London',
units: 'celsius'
});
expect(result.content).toHaveLength(1);
expect(result.content[0].text).toContain('Temperature');
});
});
When to use - For validating tool behavior in different scenarios:
- Unit testing individual tools
- Integration testing tool chains
- Regression testing after changes
- Edge case validation
Why it matters: Ensures reliability and predictability when tools are used individually or in combination.
Best Practices for Tool Composition:¶
- Single Responsibility: Each tool should do one thing well
- Consistent Interfaces: Use similar parameter patterns across tools
- Clear Dependencies: Document which tools work well together
- Error Propagation: Handle failures gracefully in tool chains
- State Management: Avoid tools that require complex state between calls
- Flexible Outputs: Design tool outputs to be usable as inputs for other tools
- Documentation: Clearly explain how tools can be combined
- Version Compatibility: Ensure tool interfaces remain stable
Best Practices Checklist¶
Schema Design
- Use descriptive names and descriptions
- Add examples in descriptions
- Set reasonable defaults
- Use enums for constrained values
- Add min/max for numbers
Implementation
- Validate all inputs, even with schemas
- Handle errors gracefully
- Log to stderr, not stdout
- Use async/await properly
- Clean up resources (file handles, connections)
Security
- Validate and sanitize file paths
- Use prepared statements for SQL
- Limit resource usage (file sizes, API calls)
- Don’t expose sensitive data in errors
- Implement rate limiting
Performance
- Cache expensive operations
- Set reasonable timeouts
- Limit result sizes
- Use streaming for large data
- Monitor execution time
User Experience
- Provide clear error messages
- Return structured data when possible
- Include relevant context in responses
- Handle edge cases gracefully
- Document expected behavior
Hands-On Exercises¶
Exercise 1: Text Processing Tool¶
Create a tool that:
- Counts words, characters, lines
- Finds specific patterns
- Calculates reading time
- Detects language
💡 Solution: Text Processing Tool
Tool Schema - Add this to your tools array:{
name: "process_text",
description: "Analyze and process text content with various metrics and operations",
inputSchema: {
type: "object",
properties: {
text: {
type: "string",
description: "The text content to process"
},
operations: {
type: "array",
description: "Operations to perform",
items: {
type: "string",
enum: ["count", "find_pattern", "reading_time", "detect_language"]
},
default: ["count"]
},
pattern: {
type: "string",
description: "Regex pattern for find_pattern operation"
}
},
required: ["text"]
}
}
if (name === "process_text") {
try {
const text = args.text as string;
const operations = (args.operations as string[]) || ["count"];
const pattern = args.pattern as string;
let results: string[] = [];
for (const op of operations) {
switch (op) {
case "count":
const lines = text.split('\n').length;
const words = text.split(/\s+/).filter(w => w.length > 0).length;
const chars = text.length;
results.push(`📊 Text Statistics:\n- Lines: ${lines}\n- Words: ${words}\n- Characters: ${chars}`);
break;
case "find_pattern":
if (!pattern) {
results.push("❌ Pattern required for find_pattern operation");
break;
}
try {
const regex = new RegExp(pattern, 'g');
const matches = text.match(regex);
results.push(`🔍 Pattern Matches (${pattern}):\nFound ${matches ? matches.length : 0} matches:\n${matches ? matches.slice(0, 10).join('\n') : 'None'}`);
} catch (e) {
results.push(`❌ Invalid regex pattern: ${pattern}`);
}
break;
case "reading_time":
// Average reading speed: 200 words per minute
const wordCount = text.split(/\s+/).filter(w => w.length > 0).length;
const readingTime = Math.ceil(wordCount / 200);
results.push(`⏱️ Reading Time: Approximately ${readingTime} minute${readingTime !== 1 ? 's' : ''} (${wordCount} words at 200 WPM)`);
break;
case "detect_language":
// Simple language detection based on common words
const englishWords = /\b(the|and|or|but|in|on|at|to|for|of|with|by)\b/gi;
const spanishWords = /\b(el|la|los|las|y|o|pero|en|sobre|a|para|de|con|por)\b/gi;
const frenchWords = /\b(le|la|les|et|ou|mais|dans|sur|à|pour|de|avec|par)\b/gi;
const englishMatches = (text.match(englishWords) || []).length;
const spanishMatches = (text.match(spanishWords) || []).length;
const frenchMatches = (text.match(frenchWords) || []).length;
const maxMatches = Math.max(englishMatches, spanishMatches, frenchMatches);
let detectedLang = "Unknown";
if (maxMatches > 0) {
if (englishMatches === maxMatches) detectedLang = "English";
else if (spanishMatches === maxMatches) detectedLang = "Spanish";
else if (frenchMatches === maxMatches) detectedLang = "French";
}
results.push(`🌍 Detected Language: ${detectedLang} (confidence: ${maxMatches} common words)`);
break;
}
}
return {
content: [
{
type: "text",
text: results.join('\n\n')
}
]
};
} catch (error) {
throw new Error(`Text processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Basic counting
{ text: "Hello world\nThis is a test", operations: ["count"] }
// Pattern matching
{ text: "The quick brown fox jumps over the lazy dog", operations: ["find_pattern"], pattern: "\\b\\w{4}\\b" }
// Multiple operations
{ text: "This is a longer piece of text to analyze for various metrics and patterns.", operations: ["count", "reading_time", "detect_language"] }
Exercise 2: JSON Validator Tool¶
Create a tool that:
- Validates JSON syntax
- Validates against JSON Schema
- Formats/pretty-prints JSON
- Compares two JSON objects
💡 Solution: JSON Validator Tool
Tool Schema - Add this to your tools array:{
name: "validate_json",
description: "Validate, format, and compare JSON data",
inputSchema: {
type: "object",
properties: {
json: {
type: "string",
description: "JSON string to validate or format"
},
operation: {
type: "string",
description: "Operation to perform",
enum: ["validate", "format", "schema_validate", "compare"],
default: "validate"
},
schema: {
type: "string",
description: "JSON Schema for schema validation (as JSON string)"
},
json2: {
type: "string",
description: "Second JSON string for comparison"
}
},
required: ["json", "operation"]
}
}
if (name === "validate_json") {
try {
const json = args.json as string;
const operation = args.operation as string;
const schema = args.schema as string;
const json2 = args.json2 as string;
let result = "";
switch (operation) {
case "validate":
try {
JSON.parse(json);
result = "✅ Valid JSON syntax";
} catch (e) {
result = `❌ Invalid JSON: ${e instanceof Error ? e.message : 'Unknown error'}`;
}
break;
case "format":
try {
const parsed = JSON.parse(json);
result = `📄 Formatted JSON:\n\`\`\`json\n${JSON.stringify(parsed, null, 2)}\n\`\`\``;
} catch (e) {
result = `❌ Cannot format invalid JSON: ${e instanceof Error ? e.message : 'Unknown error'}`;
}
break;
case "schema_validate":
if (!schema) {
result = "❌ Schema required for schema validation";
break;
}
try {
const ajv = new Ajv();
const parsedJson = JSON.parse(json);
const parsedSchema = JSON.parse(schema);
const validate = ajv.compile(parsedSchema);
const valid = validate(parsedJson);
if (valid) {
result = "✅ JSON validates against schema";
} else {
result = `❌ Schema validation failed:\n${JSON.stringify(validate.errors, null, 2)}`;
}
} catch (e) {
result = `❌ Schema validation error: ${e instanceof Error ? e.message : 'Unknown error'}`;
}
break;
case "compare":
if (!json2) {
result = "❌ Second JSON required for comparison";
break;
}
try {
const obj1 = JSON.parse(json);
const obj2 = JSON.parse(json2);
const differences: string[] = [];
// Simple comparison - check if objects are equal
if (JSON.stringify(obj1) === JSON.stringify(obj2)) {
result = "✅ JSON objects are identical";
} else {
// Find differences
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
const added = keys2.filter(k => !keys1.includes(k));
const removed = keys1.filter(k => !keys2.includes(k));
const modified = keys1.filter(k => keys2.includes(k) && JSON.stringify(obj1[k]) !== JSON.stringify(obj2[k]));
if (added.length > 0) differences.push(`Added keys: ${added.join(', ')}`);
if (removed.length > 0) differences.push(`Removed keys: ${removed.join(', ')}`);
if (modified.length > 0) differences.push(`Modified keys: ${modified.join(', ')}`);
result = `⚠️ JSON objects differ:\n${differences.join('\n')}`;
}
} catch (e) {
result = `❌ Comparison error: ${e instanceof Error ? e.message : 'Unknown error'}`;
}
break;
}
return {
content: [
{
type: "text",
text: result
}
]
};
} catch (error) {
throw new Error(`JSON validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Validate syntax
{ json: '{"name": "test", "value": 123}', operation: "validate" }
// Format JSON
{ json: '{"name":"test","value":123}', operation: "format" }
// Schema validation
{
json: '{"name": "John", "age": 30}',
operation: "schema_validate",
schema: '{"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "number"}}}'
}
// Compare JSON
{
json: '{"a": 1, "b": 2}',
json2: '{"a": 1, "c": 3}',
operation: "compare"
}
Exercise 3: Web Scraper Tool¶
Create a tool that:
- Fetches web page content
- Extracts specific elements
- Returns clean text
- Handles errors gracefully
💡 Solution: Web Scraper Tool
Tool Schema - Add this to your tools array:{
name: "scrape_web",
description: "Fetch and extract content from web pages",
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "URL to scrape",
format: "uri"
},
selector: {
type: "string",
description: "CSS selector to extract specific elements (optional)",
default: "body"
},
includeText: {
type: "boolean",
description: "Extract only text content (remove HTML)",
default: true
},
maxLength: {
type: "number",
description: "Maximum length of extracted content",
minimum: 100,
maximum: 10000,
default: 2000
},
timeout: {
type: "number",
description: "Request timeout in milliseconds",
minimum: 1000,
maximum: 30000,
default: 10000
}
},
required: ["url"]
}
}
if (name === "scrape_web") {
try {
const url = args.url as string;
const selector = (args.selector as string) || "body";
const includeText = (args.includeText !== false); // default true
const maxLength = (args.maxLength as number) || 2000;
const timeout = (args.timeout as number) || 10000;
// Validate URL
try {
new URL(url);
} catch {
throw new Error("Invalid URL format");
}
// Fetch the webpage
const response = await axios.get(url, {
timeout: timeout,
headers: {
'User-Agent': 'MCP-Web-Scraper/1.0 (Educational Tool)'
},
maxContentLength: 5 * 1024 * 1024, // 5MB limit
});
// Load HTML into cheerio
const $ = cheerio.load(response.data);
// Extract content based on selector
let extractedContent = "";
if (selector === "body") {
extractedContent = includeText ? $('body').text() : $('body').html() || "";
} else {
const elements = $(selector);
if (elements.length === 0) {
throw new Error(`No elements found matching selector: ${selector}`);
}
if (includeText) {
extractedContent = elements.map((_, el) => $(el).text()).get().join('\n\n');
} else {
extractedContent = elements.map((_, el) => $.html(el)).get().join('\n\n');
}
}
// Clean up the content
extractedContent = extractedContent
.replace(/\s+/g, ' ') // Replace multiple whitespace with single space
.replace(/\n\s*\n/g, '\n') // Remove empty lines
.trim();
// Truncate if too long
if (extractedContent.length > maxLength) {
extractedContent = extractedContent.substring(0, maxLength - 3) + "...";
}
// Prepare metadata
const metadata = {
url: url,
statusCode: response.status,
contentType: response.headers['content-type'],
contentLength: response.data.length,
extractedLength: extractedContent.length,
selector: selector,
elementsFound: selector === "body" ? 1 : $(selector).length
};
return {
content: [
{
type: "text",
text: `🌐 Web Scraping Results\n\n📊 Metadata:\n${Object.entries(metadata).map(([k, v]) => `- ${k}: ${v}`).join('\n')}\n\n📄 Extracted Content:\n${extractedContent}`
}
]
};
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.code === 'ENOTFOUND') {
throw new Error(`Could not resolve hostname: ${args.url}`);
} else if (error.code === 'ECONNREFUSED') {
throw new Error(`Connection refused: ${args.url}`);
} else if (error.response) {
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
} else if (error.code === 'ETIMEDOUT') {
throw new Error(`Request timeout after ${args.timeout || 10000}ms`);
} else {
throw new Error(`Network error: ${error.message}`);
}
} else {
throw new Error(`Web scraping failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
// Basic page scraping
{ url: "https://httpbin.org/html" }
// Extract specific elements
{ url: "https://httpbin.org/html", selector: "h1" }
// Get HTML instead of text
{ url: "https://httpbin.org/html", selector: "p", includeText: false }
// Test error handling
{ url: "https://nonexistent-domain-12345.com" }
{ url: "https://httpbin.org/status/404" }
Lab 6: K-Agent Integration¶
Overview¶
Welcome to the advanced MCP lab!
Now that you’ve mastered the fundamentals of MCP servers, tools, resources, and prompts, it’s time to apply your knowledge to a real-world scenario: building and implementing a K-Agent.
A K-Agent is an MCP server specifically designed to interact with Kubernetes clusters.
In this lab, you’ll build a focused K-Agent that communicates with a Kubernetes cluster and collects logs from all pods - a critical capability for monitoring, debugging, and operational intelligence.
This lab bridges the gap between MCP theory and practical Kubernetes operations, showing how MCP servers can enhance DevOps workflows and provide AI-powered insights into cluster health and application behavior.
Learning Objectives¶
By the end of this lab, you will:
- Understand the concept and value of K-Agents (Kubernetes MCP servers)
- Use pre-configured Kubernetes cluster access
- Build MCP tools for pod log collection
- Implement error handling for cluster operations
Prerequisites¶
- Completed Lab 5 - MCP Prompts and Integration
- A running Kubernetes cluster (OrbStack, Docker Desktop, Minikube, kind, or remote cluster)
- kubectl installed and configured with access to your cluster (
kubectl cluster-infoshould work) - Basic understanding of Kubernetes concepts (pods, namespaces, kubectl)
- Familiarity with container orchestration principles
- Understanding of log aggregation and monitoring concepts
Verify Kubernetes Setup
Before starting this lab, ensure your Kubernetes environment is ready:
What is a K-Agent?¶
A K-Agent is an MCP server that specializes in Kubernetes operations.
Unlike generic MCP servers, K-Agents are designed specifically for:
- Cluster Communication: Secure, authenticated access to Kubernetes APIs
- Operational Intelligence: Real-time insights into cluster health and performance
- Log Analytics: Collection and analysis of application and system logs
- Resource Management: Monitoring and managing Kubernetes resources
- Troubleshooting: Automated diagnosis of cluster and application issues
Why K-Agents Matter¶
In modern DevOps environments, Kubernetes clusters generate enormous amounts of operational data.
K-Agents provide:
- AI-Powered Monitoring: Intelligent analysis of logs and metrics
- Automated Troubleshooting: AI-assisted diagnosis of issues
- Operational Insights: Pattern recognition in cluster behavior
- Enhanced Observability: Structured access to distributed system data
Lab Architecture¶
Your K-Agent will implement a focused set of capabilities:
graph TB
subgraph "MCP Client (AI/Inspector)"
Client[MCP Client]
end
subgraph "K-Agent MCP Server"
Server[K-Agent Server]
Tools[MCP Tools]
K8sClient[Kubernetes Client]
Server --> Tools
Tools --> K8sClient
end
subgraph "Kubernetes Cluster"
API[Kubernetes API]
Pods[Pods]
Logs[Container Logs]
API --> Pods
Pods --> Logs
end
Client -->|"stdio/JSON-RPC"| Server
K8sClient -->|"REST API"| API
Tools -.->|"list_pods"| API
Tools -.->|"collect_pod_logs"| Logs
style Server fill:#4CAF50
style Tools fill:#2196F3
style K8sClient fill:#FF9800
style API fill:#9C27B0
Core Components¶
-
Kubernetes Client Integration
- Secure cluster authentication
- API communication handling
- Error management for cluster operations
-
Log Collection Tools
- Pod discovery across namespaces
- Log retrieval from all containers
- Structured log formatting
-
Resource Management
- Namespace enumeration
- Pod status monitoring
- Health check capabilities
Hands-On Exercise¶
Step 1: Project Setup¶
Create a new MCP server project with Kubernetes dependencies:
# Create project directory and navigate to it
mkdir k-agent-logs # <-- Create this directory next to the previous labs directories
cd k-agent-logs
Create a new package.json file inside the k-agent-logs directory with the following content:
{
"name": "k-agent-logs",
"version": "1.0.0",
"description": "K-Agent MCP server for Kubernetes log collection",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js"
},
"keywords": ["kubernetes", "mcp", "logs", "monitoring"],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@kubernetes/client-node": "^1.4.0",
"@modelcontextprotocol/sdk": "^1.25.2"
},
"devDependencies": {
"@types/node": "^25.0.3",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
}
}
Run the following command to install the dependencies:
This will create node_modules/ and package-lock.json in your k-agent-logs directory.
Step 2: Kubernetes Client Configuration & Complete Server Setup¶
Create an src directory, inside the k-agent-logs directory, and an empty file named index.ts inside it:
Create your complete K-Agent server by pasting the following code inside src/index.ts:
// Import MCP SDK components and Kubernetes client
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListResourcesRequestSchema,
ListToolsRequestSchema,
McpError,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as k8s from '@kubernetes/client-node';
class KAgentServer {
// Store Kubernetes API clients (for talking to your cluster)
private k8sConfig: k8s.KubeConfig;
private k8sAppsApi: k8s.AppsV1Api;
private k8sCoreApi: k8s.CoreV1Api;
private server: Server;
constructor() {
// Initialize connection to your Kubernetes cluster (uses ~/.kube/config)
this.k8sConfig = new k8s.KubeConfig();
this.k8sConfig.loadFromDefault();
this.k8sAppsApi = this.k8sConfig.makeApiClient(k8s.AppsV1Api);
this.k8sCoreApi = this.k8sConfig.makeApiClient(k8s.CoreV1Api);
// Create MCP server that AI tools can connect to
this.server = new Server(
{
name: "k-agent-logs",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Handle incoming MCP requests (you'll add tools here next)
this.setupHandlers();
}
private setupHandlers() {
// TODO: Implement MCP handlers
}
// Start the server and listen for connections
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("K-Agent MCP server running on stdio");
}
}
// Actually run the server
const server = new KAgentServer();
server.run().catch(console.error);
Create a file named tsconfig.json inside the k-agent-logs directory (not inside src):
Paste the following content into tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Test that it works:
You should see: K-Agent MCP server running on stdio
If you get errors:
Missing script: "dev"→ You didn’t update package.json (go back to step 2)Cannot find module→ Make sure you’re in the k-agent-logs directory- Other errors → Check that src/index.ts has the correct code
Step 3: Pod Discovery Tool¶
Implement pod enumeration across namespaces with the list_pods tool.
Update setupHandlers() Method¶
Open your src/index.ts file and find the setupHandlers() method. Replace the entire method with the following code:
private setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_pods",
description: "List all pods across namespaces with their status",
inputSchema: {
type: "object",
properties: {
namespace: {
type: "string",
description: "Optional: Filter by specific namespace"
}
}
}
}
]
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list_pods":
return await this.handleListPods(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
Add Handler Methods¶
Below the setupHandlers() method, before the async run() method, add the following new methods:
private async handleListPods(args: any) {
const namespace = args?.namespace;
const pods = await this.getPods(namespace);
const podList = pods.map(pod => ({
name: pod.metadata?.name || 'unknown',
namespace: pod.metadata?.namespace || 'unknown',
status: pod.status?.phase || 'unknown',
containers: pod.spec?.containers?.map(c => c.name) || []
}));
return {
content: [
{
type: "text",
text: JSON.stringify(podList, null, 2)
}
]
};
}
private async getPods(namespace?: string): Promise<k8s.V1Pod[]> {
try {
if (namespace) {
const response = await this.k8sCoreApi.listNamespacedPod({ namespace });
return response.items || [];
} else {
const response = await this.k8sCoreApi.listPodForAllNamespaces();
return response.items || [];
}
} catch (error) {
throw this.handleK8sError(error);
}
}
private handleK8sError(error: any): Error {
if (error.response?.statusCode === 403) {
return new Error('Access denied: Insufficient permissions to access Kubernetes resources');
}
if (error.response?.statusCode === 404) {
return new Error('Resource not found: The specified pod or namespace may not exist');
}
return new Error(`Kubernetes operation failed: ${error.message}`);
}
Complete src/index.ts After Step 3
Here’s what your complete src/index.ts file should look like after completing Step 3:
// Import MCP SDK components and Kubernetes client
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListResourcesRequestSchema,
ListToolsRequestSchema,
McpError,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as k8s from '@kubernetes/client-node';
class KAgentServer {
// Store Kubernetes API clients (for talking to your cluster)
private k8sConfig: k8s.KubeConfig;
private k8sAppsApi: k8s.AppsV1Api;
private k8sCoreApi: k8s.CoreV1Api;
private server: Server;
constructor() {
// Initialize connection to your Kubernetes cluster (uses ~/.kube/config)
this.k8sConfig = new k8s.KubeConfig();
this.k8sConfig.loadFromDefault();
this.k8sAppsApi = this.k8sConfig.makeApiClient(k8s.AppsV1Api);
this.k8sCoreApi = this.k8sConfig.makeApiClient(k8s.CoreV1Api);
// Create MCP server that AI tools can connect to
this.server = new Server(
{
name: "k-agent-logs",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Handle incoming MCP requests
this.setupHandlers();
}
private setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_pods",
description: "List all pods across namespaces with their status",
inputSchema: {
type: "object",
properties: {
namespace: {
type: "string",
description: "Optional: Filter by specific namespace"
}
}
}
}
]
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list_pods":
return await this.handleListPods(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
private async handleListPods(args: any) {
const namespace = args?.namespace;
const pods = await this.getPods(namespace);
const podList = pods.map(pod => ({
name: pod.metadata?.name || 'unknown',
namespace: pod.metadata?.namespace || 'unknown',
status: pod.status?.phase || 'unknown',
containers: pod.spec?.containers?.map(c => c.name) || []
}));
return {
content: [
{
type: "text",
text: JSON.stringify(podList, null, 2)
}
]
};
}
private async getPods(namespace?: string): Promise<k8s.V1Pod[]> {
try {
if (namespace) {
const response = await this.k8sCoreApi.listNamespacedPod({ namespace });
return response.items || [];
} else {
const response = await this.k8sCoreApi.listPodForAllNamespaces();
return response.items || [];
}
} catch (error) {
throw this.handleK8sError(error);
}
}
private handleK8sError(error: any): Error {
if (error.response?.statusCode === 403) {
return new Error('Access denied: Insufficient permissions to access Kubernetes resources');
}
if (error.response?.statusCode === 404) {
return new Error('Resource not found: The specified pod or namespace may not exist');
}
return new Error(`Kubernetes operation failed: ${error.message}`);
}
// Start the server and listen for connections
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("K-Agent MCP server running on stdio");
}
}
// Actually run the server
const server = new KAgentServer();
server.run().catch(console.error);
Test Pod Discovery¶
Test the pod listing functionality using the MCP Inspector by running:
This will start the MCP Inspector connected to your K-Agent server and open a browser window.
Inside the MCP Inspector UI browser window:
- Click the “Connect” button
- Click the “Tools” tab
- Click “List Tools” - you’ll see the
list_podstool - Click on
list_pods - Optionally enter a namespace name
- Click “Run Tool” to test it
You should see a JSON list of all pods with their status and container names.
Step 4: Log Collection Tool¶
Build the core log collection functionality with the collect_pod_logs tool.
Add collect_pod_logs to Tools Array¶
In your src/index.ts file, locate the tools array inside setupHandlers() method.
Add the following second tool code to the array:
private setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_pods",
description: "List all pods across namespaces with their status",
inputSchema: {
type: "object",
properties: {
namespace: {
type: "string",
description: "Optional: Filter by specific namespace"
}
}
}
},
{
name: "collect_pod_logs",
description: "Collect logs from all containers in specified pods",
inputSchema: {
type: "object",
properties: {
namespace: {
type: "string",
description: "Namespace to collect logs from"
},
podName: {
type: "string",
description: "Specific pod name (optional - collects from all if not specified)"
},
tailLines: {
type: "number",
description: "Number of recent log lines to retrieve",
default: 100
}
},
required: ["namespace"]
}
}
]
};
});
// ... rest of setupHandlers
}
Update Tool Handler Switch¶
In the same setupHandlers() method, locate the switch statement and add the following case for collect_pod_logs:
switch (name) {
case "list_pods":
return await this.handleListPods(args);
case "collect_pod_logs":
return await this.handleCollectPodLogs(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
Implement Log Collection Methods¶
Below your existing handler methods (after handleK8sError()), before the async run() method, add the following new methods:
private async handleCollectPodLogs(args: any) {
const { namespace, podName, tailLines = 100 } = args;
if (!namespace) {
throw new Error("Namespace is required");
}
const logs = await this.collectPodLogs(namespace, podName, tailLines);
return {
content: [
{
type: "text",
text: logs
}
]
};
}
private async collectPodLogs(namespace: string, podName?: string, tailLines: number = 100): Promise<string> {
const pods = podName
? await this.getPods(namespace).then(pods => pods.filter(p => p.metadata?.name === podName))
: await this.getPods(namespace);
const allLogs: string[] = [];
for (const pod of pods) {
if (!pod.metadata?.name) continue;
const containers = pod.spec?.containers || [];
for (const container of containers) {
try {
const logs = await this.getPodLogs(namespace, pod.metadata.name, container.name, tailLines);
allLogs.push(`=== ${pod.metadata.name}/${container.name} ===\n${logs}\n`);
} catch (error) {
allLogs.push(`=== ${pod.metadata.name}/${container.name} ===\nError retrieving logs: ${error instanceof Error ? error.message : String(error)}\n`);
}
}
}
return allLogs.join('\n');
}
private async getPodLogs(namespace: string, podName: string, containerName: string, tailLines: number): Promise<string> {
try {
const response = await this.k8sCoreApi.readNamespacedPodLog({
name: podName,
namespace: namespace,
container: containerName,
tailLines: tailLines,
timestamps: true
});
return response || '';
} catch (error) {
throw this.handleK8sError(error);
}
}
Complete src/index.ts After Step 4
Here’s what your complete src/index.ts file should look like after completing Step 4:
// Import MCP SDK components and Kubernetes client
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListResourcesRequestSchema,
ListToolsRequestSchema,
McpError,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as k8s from '@kubernetes/client-node';
class KAgentServer {
// Store Kubernetes API clients (for talking to your cluster)
private k8sConfig: k8s.KubeConfig;
private k8sAppsApi: k8s.AppsV1Api;
private k8sCoreApi: k8s.CoreV1Api;
private server: Server;
constructor() {
// Initialize connection to your Kubernetes cluster (uses ~/.kube/config)
this.k8sConfig = new k8s.KubeConfig();
this.k8sConfig.loadFromDefault();
this.k8sAppsApi = this.k8sConfig.makeApiClient(k8s.AppsV1Api);
this.k8sCoreApi = this.k8sConfig.makeApiClient(k8s.CoreV1Api);
// Create MCP server that AI tools can connect to
this.server = new Server(
{
name: "k-agent-logs",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Handle incoming MCP requests
this.setupHandlers();
}
private setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_pods",
description: "List all pods across namespaces with their status",
inputSchema: {
type: "object",
properties: {
namespace: {
type: "string",
description: "Optional: Filter by specific namespace"
}
}
}
},
{
name: "collect_pod_logs",
description: "Collect logs from all containers in specified pods",
inputSchema: {
type: "object",
properties: {
namespace: {
type: "string",
description: "Namespace to collect logs from"
},
podName: {
type: "string",
description: "Specific pod name (optional - collects from all if not specified)"
},
tailLines: {
type: "number",
description: "Number of recent log lines to retrieve",
default: 100
}
},
required: ["namespace"]
}
}
]
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list_pods":
return await this.handleListPods(args);
case "collect_pod_logs":
return await this.handleCollectPodLogs(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
private async handleListPods(args: any) {
const namespace = args?.namespace;
const pods = await this.getPods(namespace);
const podList = pods.map(pod => ({
name: pod.metadata?.name || 'unknown',
namespace: pod.metadata?.namespace || 'unknown',
status: pod.status?.phase || 'unknown',
containers: pod.spec?.containers?.map(c => c.name) || []
}));
return {
content: [
{
type: "text",
text: JSON.stringify(podList, null, 2)
}
]
};
}
private async handleCollectPodLogs(args: any) {
const { namespace, podName, tailLines = 100 } = args;
if (!namespace) {
throw new Error("Namespace is required");
}
const logs = await this.collectPodLogs(namespace, podName, tailLines);
return {
content: [
{
type: "text",
text: logs
}
]
};
}
private async collectPodLogs(namespace: string, podName?: string, tailLines: number = 100): Promise<string> {
const pods = podName
? await this.getPods(namespace).then(pods => pods.filter(p => p.metadata?.name === podName))
: await this.getPods(namespace);
const allLogs: string[] = [];
for (const pod of pods) {
if (!pod.metadata?.name) continue;
const containers = pod.spec?.containers || [];
for (const container of containers) {
try {
const logs = await this.getPodLogs(namespace, pod.metadata.name, container.name, tailLines);
allLogs.push(`=== ${pod.metadata.name}/${container.name} ===\n${logs}\n`);
} catch (error) {
allLogs.push(`=== ${pod.metadata.name}/${container.name} ===\nError retrieving logs: ${error instanceof Error ? error.message : String(error)}\n`);
}
}
}
return allLogs.join('\n');
}
private async getPods(namespace?: string): Promise<k8s.V1Pod[]> {
try {
if (namespace) {
const response = await this.k8sCoreApi.listNamespacedPod({ namespace });
return response.items || [];
} else {
const response = await this.k8sCoreApi.listPodForAllNamespaces();
return response.items || [];
}
} catch (error) {
throw this.handleK8sError(error);
}
}
private async getPodLogs(namespace: string, podName: string, containerName: string, tailLines: number): Promise<string> {
try {
const response = await this.k8sCoreApi.readNamespacedPodLog({
name: podName,
namespace: namespace,
container: containerName,
tailLines: tailLines,
timestamps: true
});
return response || '';
} catch (error) {
throw this.handleK8sError(error);
}
}
private handleK8sError(error: any): Error {
if (error.response?.statusCode === 403) {
return new Error('Access denied: Insufficient permissions to access Kubernetes resources');
}
if (error.response?.statusCode === 404) {
return new Error('Resource not found: The specified pod or namespace may not exist');
}
return new Error(`Kubernetes operation failed: ${error.message}`);
}
// Start the server and listen for connections
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("K-Agent MCP server running on stdio");
}
}
// Actually run the server
const server = new KAgentServer();
server.run().catch(console.error);
Test Log Collection¶
Test the log collection functionality with the MCP Inspector:
This will start the MCP Inspector connected to your K-Agent server and open a browser window.
In the MCP Inspector UI browser window:
- Click the “Connect” button
- Click the “Tools” tab
- Click “List Tools” - you’ll see both
list_podsandcollect_pod_logs - Click on
collect_pod_logs - In the namespace field, enter:
default - Click “Run Tool”
You should see logs from all pods in the default namespace, formatted with pod and container names.
Optional Extensions¶
Prerequisites
These exercises assume you have completed Steps 1-4 and have a fully functional K-Agent server with list_pods and collect_pod_logs tools. These exercises extend the functionality beyond the core implementation.
Exercise 1: Advanced Pod Filtering and Sorting¶
Enhance the list_pods tool to support filtering by status, labels, and sorting by various criteria.
Update the list_pods tool schema in your setupHandlers() method to include new parameters:
{
name: "list_pods",
description: "List all pods across namespaces with their status, with filtering and sorting options",
inputSchema: {
type: "object",
properties: {
namespace: {
type: "string",
description: "Optional: Filter by specific namespace"
},
status: {
type: "string",
description: "Optional: Filter by pod status (Running, Pending, Failed, etc.)",
enum: ["Running", "Pending", "Failed", "Succeeded", "Unknown"]
},
labelSelector: {
type: "string",
description: "Optional: Filter by label selector (e.g., 'app=nginx,env=prod')"
},
sortBy: {
type: "string",
description: "Sort results by field",
enum: ["name", "namespace", "status", "age"],
default: "name"
}
}
}
}
Update the handleListPods method to support the new parameters:
private async handleListPods(args: any) {
const { namespace, status, labelSelector, sortBy = 'name' } = args;
let pods = await this.getPods(namespace, labelSelector);
// Filter by status if specified
if (status) {
pods = pods.filter(pod => pod.status?.phase === status);
}
// Map to simplified format
const podList = pods.map(pod => ({
name: pod.metadata?.name || 'unknown',
namespace: pod.metadata?.namespace || 'unknown',
status: pod.status?.phase || 'unknown',
containers: pod.spec?.containers?.map(c => c.name) || [],
age: pod.metadata?.creationTimestamp
? Math.floor((Date.now() - new Date(pod.metadata.creationTimestamp).getTime()) / 1000 / 60)
: 0,
labels: pod.metadata?.labels || {}
}));
// Sort results
const sortedPods = this.sortPods(podList, sortBy);
return {
content: [
{
type: "text",
text: JSON.stringify(sortedPods, null, 2)
}
]
};
}
Update the getPods method to support label selectors:
private async getPods(namespace?: string, labelSelector?: string): Promise<k8s.V1Pod[]> {
try {
if (namespace) {
const response = await this.k8sCoreApi.listNamespacedPod({
namespace,
labelSelector
});
return response.items || [];
} else {
const response = await this.k8sCoreApi.listPodForAllNamespaces({
labelSelector
});
return response.items || [];
}
} catch (error) {
throw this.handleK8sError(error);
}
}
Add a sorting helper method after the getPods method:
private sortPods(pods: any[], sortBy: string): any[] {
return pods.sort((a, b) => {
switch (sortBy) {
case 'namespace':
return a.namespace.localeCompare(b.namespace);
case 'status':
return a.status.localeCompare(b.status);
case 'age':
return b.age - a.age; // Newest first
case 'name':
default:
return a.name.localeCompare(b.name);
}
});
}
Test the enhanced filtering:
Try these test cases:
- List only Running pods: Set
statusto “Running” - Filter by labels: Set
labelSelectorto “app=nginx” - Sort by age: Set
sortByto “age” - Combine filters: Use namespace + status + sortBy together
Exercise 2: Enhanced Log Analysis with Search and Export¶
Extend the collect_pod_logs tool to support log searching, filtering by severity, and exporting logs to files.
Add a new tool analyze_logs to your tools array in setupHandlers():
{
name: "analyze_logs",
description: "Analyze and search through pod logs with filtering and export capabilities",
inputSchema: {
type: "object",
properties: {
namespace: {
type: "string",
description: "Namespace to analyze logs from"
},
podName: {
type: "string",
description: "Specific pod name (optional)"
},
searchPattern: {
type: "string",
description: "Regex pattern to search for in logs"
},
severityLevel: {
type: "string",
description: "Filter by log severity",
enum: ["ERROR", "WARN", "INFO", "DEBUG"]
},
tailLines: {
type: "number",
description: "Number of recent log lines to analyze",
default: 100
},
exportToFile: {
type: "boolean",
description: "Export matching logs to a file",
default: false
}
},
required: ["namespace"]
}
}
Add the case to your switch statement:
Implement the log analysis handler after your handleCollectPodLogs method:
private async handleAnalyzeLogs(args: any) {
const { namespace, podName, searchPattern, severityLevel, tailLines = 100, exportToFile = false } = args;
if (!namespace) {
throw new Error("Namespace is required");
}
const logs = await this.collectPodLogs(namespace, podName, tailLines);
const analyzedLogs = this.analyzeLogs(logs, searchPattern, severityLevel);
if (exportToFile) {
const filename = await this.exportLogs(analyzedLogs, namespace, podName);
return {
content: [
{
type: "text",
text: `Analysis complete. ${analyzedLogs.totalLines} lines analyzed, ${analyzedLogs.matchingLines} matches found.\nExported to: ${filename}\n\n${analyzedLogs.summary}`
}
]
};
}
return {
content: [
{
type: "text",
text: `Analysis Results:\n${analyzedLogs.summary}\n\nMatching Logs:\n${analyzedLogs.matches.join('\n')}`
}
]
};
}
Add the log analysis helper method:
private analyzeLogs(logs: string, searchPattern?: string, severityLevel?: string) {
const lines = logs.split('\n');
const matches: string[] = [];
const errors: string[] = [];
const warnings: string[] = [];
for (const line of lines) {
// Filter by severity if specified
if (severityLevel) {
if (!line.includes(severityLevel)) continue;
}
// Search for pattern if specified
if (searchPattern) {
const regex = new RegExp(searchPattern, 'i');
if (regex.test(line)) {
matches.push(line);
}
} else {
matches.push(line);
}
// Categorize by severity
if (line.includes('ERROR') || line.includes('error')) {
errors.push(line);
} else if (line.includes('WARN') || line.includes('warning')) {
warnings.push(line);
}
}
const summary = [
`Total Lines: ${lines.length}`,
`Matching Lines: ${matches.length}`,
`Errors Found: ${errors.length}`,
`Warnings Found: ${warnings.length}`,
searchPattern ? `Search Pattern: ${searchPattern}` : '',
severityLevel ? `Severity Filter: ${severityLevel}` : ''
].filter(Boolean).join('\n');
return {
totalLines: lines.length,
matchingLines: matches.length,
matches: matches.slice(0, 100), // Limit to first 100 matches
errors,
warnings,
summary
};
}
Add the export helper method (requires fs module - add import * as fs from 'fs'; at the top):
private async exportLogs(analyzedLogs: any, namespace: string, podName?: string): Promise<string> {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `logs_${namespace}_${podName || 'all'}_${timestamp}.txt`;
const filepath = `/tmp/${filename}`;
const content = [
`Kubernetes Log Analysis Report`,
`Generated: ${new Date().toISOString()}`,
`Namespace: ${namespace}`,
podName ? `Pod: ${podName}` : 'All Pods',
``,
analyzedLogs.summary,
``,
`=== Matching Log Entries ===`,
analyzedLogs.matches.join('\n')
].join('\n');
// Note: In a real implementation, you'd want to handle file system operations more carefully
// For this exercise, we'll just return the intended filename
return filepath;
}
Test the log analysis:
Try these test cases:
- Search for errors: Set
searchPatternto “error” andseverityLevelto “ERROR” - Find warnings: Set
severityLevelto “WARN” - Export logs: Set
exportToFileto true - Pattern matching: Set
searchPatternto a specific error code or message
Exercise 3: Real-Time Pod Monitoring¶
Create a new tool to monitor pod health and events in real-time, providing insights into pod lifecycle changes.
Add the monitor_pod_health tool to your tools array:
{
name: "monitor_pod_health",
description: "Monitor pod health, restart counts, and recent events",
inputSchema: {
type: "object",
properties: {
namespace: {
type: "string",
description: "Namespace to monitor"
},
podName: {
type: "string",
description: "Specific pod to monitor (optional)"
},
includeEvents: {
type: "boolean",
description: "Include recent Kubernetes events",
default: true
}
},
required: ["namespace"]
}
}
Add the case to your switch statement:
Implement the monitoring handler:
private async handleMonitorPodHealth(args: any) {
const { namespace, podName, includeEvents = true } = args;
if (!namespace) {
throw new Error("Namespace is required");
}
const pods = podName
? await this.getPods(namespace).then(pods => pods.filter(p => p.metadata?.name === podName))
: await this.getPods(namespace);
const healthReports = [];
for (const pod of pods) {
if (!pod.metadata?.name) continue;
const health = this.analyzePodHealth(pod);
const events = includeEvents
? await this.getPodEvents(namespace, pod.metadata.name)
: [];
healthReports.push({
pod: pod.metadata.name,
namespace: pod.metadata.namespace,
health,
events
});
}
return {
content: [
{
type: "text",
text: JSON.stringify(healthReports, null, 2)
}
]
};
}
Add the health analysis method:
private analyzePodHealth(pod: k8s.V1Pod) {
const status = pod.status;
const conditions = status?.conditions || [];
const containerStatuses = status?.containerStatuses || [];
// Calculate restart counts
const totalRestarts = containerStatuses.reduce(
(sum, cs) => sum + (cs.restartCount || 0),
0
);
// Check readiness
const readyCondition = conditions.find(c => c.type === 'Ready');
const isReady = readyCondition?.status === 'True';
// Check container states
const containerHealth = containerStatuses.map(cs => ({
name: cs.name,
ready: cs.ready,
restartCount: cs.restartCount || 0,
state: cs.state?.running ? 'Running'
: cs.state?.waiting ? `Waiting: ${cs.state.waiting.reason}`
: cs.state?.terminated ? `Terminated: ${cs.state.terminated.reason}`
: 'Unknown',
lastState: cs.lastState?.terminated
? `Previously terminated: ${cs.lastState.terminated.reason}`
: undefined
}));
// Overall health assessment
const healthStatus =
status?.phase === 'Running' && isReady && totalRestarts === 0 ? 'Healthy' :
status?.phase === 'Running' && totalRestarts > 0 ? 'Unstable' :
status?.phase === 'Pending' ? 'Starting' :
status?.phase === 'Failed' ? 'Failed' :
'Unknown';
return {
phase: status?.phase,
healthStatus,
isReady,
totalRestarts,
age: pod.metadata?.creationTimestamp
? Math.floor((Date.now() - new Date(pod.metadata.creationTimestamp).getTime()) / 1000 / 60)
: 0,
conditions: conditions.map(c => ({
type: c.type,
status: c.status,
reason: c.reason,
message: c.message
})),
containers: containerHealth
};
}
Add the events retrieval method:
private async getPodEvents(namespace: string, podName: string): Promise<any[]> {
try {
const response = await this.k8sCoreApi.listNamespacedEvent({
namespace,
fieldSelector: `involvedObject.name=${podName}`
});
const events = (response.items || [])
.sort((a, b) => {
const timeA = a.lastTimestamp || a.firstTimestamp || '';
const timeB = b.lastTimestamp || b.firstTimestamp || '';
return timeB.localeCompare(timeA);
})
.slice(0, 10) // Last 10 events
.map(event => ({
type: event.type,
reason: event.reason,
message: event.message,
count: event.count,
time: event.lastTimestamp || event.firstTimestamp
}));
return events;
} catch (error) {
console.error('Failed to retrieve events:', error);
return [];
}
}
Test the monitoring functionality:
Try these test cases:
- Monitor a specific pod: Set
namespaceandpodName - Check all pods in namespace: Set only
namespace - Include events: Set
includeEventsto true - Look for unhealthy pods with high restart counts
Extension Ideas
Now that you have a robust K-Agent, consider these additional enhancements:
- Resource Metrics: Add CPU/memory usage monitoring using the Metrics API
- Multi-Cluster Support: Extend to work with multiple Kubernetes clusters
- Alerting: Implement threshold-based alerts for restart counts or error rates
- Log Aggregation: Integrate with log aggregation systems like ELK or Loki
- Historical Analysis: Store and analyze trends over time
- Auto-Remediation: Add tools to automatically restart or scale problematic pods
Troubleshooting¶
Common Issues¶
“Kubernetes configuration not found”
- Ensure kubectl is installed and configured
- Check ~/.kube/config exists and is readable
“Pod not found” errors
- Confirm pod names and namespaces are correct
- Check pod status with kubectl get pods
Debug Commands¶
# Check cluster access
kubectl cluster-info
# Verify service account permissions
kubectl auth can-i list pods --as=system:serviceaccount:default:k-agent-sa
# Check pod logs
kubectl logs -n your-namespace your-pod-name
Key Takeaways¶
What You Learned
- K-Agent Architecture: Building specialized MCP servers for Kubernetes operations
- MCP Server Setup: Configuring TypeScript-based MCP servers with proper dependencies
- Kubernetes Client Integration: Using the @kubernetes/client-node library to interact with clusters
- Tool Implementation: Creating MCP tools for pod discovery and log collection
- Error Handling: Managing Kubernetes API errors and edge cases
- Testing with MCP Inspector: Using the Inspector to test and debug MCP tools
Congratulations! You’ve built a functional K-Agent with two core capabilities:
- Pod Discovery - List all pods across namespaces with status information
- Log Collection - Retrieve logs from pod containers with timestamp support
What’s Next?¶
Your K-Agent provides a foundation for more advanced Kubernetes automation.
Consider exploring:
- Adding filtering, log analysis, and health monitoring
- Integrating your K-Agent with other MCP clients
- Adding more tools for resource management (deployments, services, configmaps)
- Implementing resource watching for real-time cluster monitoring
- Building custom tools specific to your set Kubernetes workflows
Additional Resources¶
Tasks
List of tasks¶
Task: Install and Setup Roo Code¶
- This guide outlines the steps to install Roo Code (formerly Cline), configure it with custom instructions and MCP (Model Context Protocol), and demonstrates a usage example with a hypothetical “Context7” MCP server.
1. Install Roo Code¶
Roo Code is an autonomous coding agent extension for VS Code.
- Open VS Code.
- Go to the Extensions view (Click the square icon on the sidebar or press
Cmd+Shift+X). - Search for “Roo Code”.
- Look for the extension published by RooVeterinaryInc (ID:
RooVeterinaryInc.roo-cline). - Click Install.
2. Setup Roo Code¶
- Once installed, you need to configure Roo Code.
- Open the Roo Code sidebar by clicking the Roo Code icon on the sidebar.
- You will see the main Roo Code interface.
- We will go over the key configuration options during the lecture.
-
Adding API Keys (Use exiting or create new keys as needed) https://aistudio.google.com/api-keys


3. Configuring Roo Code Features¶
Create Profile¶
Creating a Profile
- Open the Roo Code sidebar.
- Click the Profile icon (user icon).
- Click “Create Profile”.
- Fill in your details and preferences.
- Click “Save Profile”.
- Your profile will help Roo Code tailor its responses to your coding style and preferences.
- You can create multiple profiles for different projects or coding styles.
- Switch between profiles as needed.
API Keys¶
Setting Up API Keys
- Open the Roo Code sidebar.
- Click the Settings (gear icon).
- Scroll to “API Keys”.
- Enter your API keys for the models you want to use (e.g., OpenAI, Google Gemini).
- Click “Save”.
- Roo Code will use these keys to access the respective language models for code generation and assistance.
Modes¶
Setting Up Modes
- Open the Roo Code sidebar.
- Click the Settings (gear icon).
- Scroll to “Default Mode”.
- Select your preferred default mode (e.g., Code, Architect, Ask).
- Click “Save”.
- This setting determines how Roo Code will approach tasks by default.
4. Advanced Configuration¶
MCP Servers¶
- MCP (Model Context Protocol) allows Roo Code to connect to external tools and data sources.
Setting Up MCP Servers
- In the Roo Code sidebar, click the MCP icon (server icon) or go to Settings > MCP Servers.
- Click “Edit MCP Settings” (Global or Project).
- This will open a JSON file for the MCP settings.
- Add your MCP servers to the
mcpServersobject.- Each server configuration includes the command to start the server, arguments, and environment variables if needed.
- Save the file.
- Roo Code will now be able to use these MCP servers for enhanced context and capabilities.
Additional Settings¶
-
We will cover these in the lecture.
Setting Description Auto-Approve Automatically approve Roo Code’s suggested changes. Slash Commands Enable or disable slash commands for quick actions. Context Configure how much file context Roo Code uses when generating code (Tabs, limits etc.). Prompts Customize prompt templates for different modes.
4. Adding Custom Instructions¶
- Adding custom instructions to Roo Code is a powerful way to enforce coding standards, project context, or specific AI behaviors.
- There are three primary ways to do this: Project-specific files, the UI (Prompts Tab), and Global configuration.
1. Project-Specific Instructions (Recommended)¶
- This is the best way to ensure that any developer working on your specific project gets the same AI behavior.
Method A: The .roo/rules/ Directory (Modern)
-
Roo Code now looks for a directory in your project root to load instructions.
Setting Up Custom Rules
- Create a folder named
.roo/rules/in your project’s root directory. - Roo Code will read all files in this directory and apply the instructions when working on the project.
- Example filenames:
- Add any number of
.md(Markdown) files in this directory.
- Example content for
coding-standards.md: - This method allows for easy version control and sharing of instructions with your team.
- Roo Code will automatically pick up changes to these files.
- Create a folder named
2. Using the Prompts Tab in the UI (Global/Quick Edits)¶
- You can also add custom instructions directly through the Roo Code UI.
-
This method is less ideal for project-specific instructions but can be useful for quick adjustments.
Adding Custom Prompts via UI
- Open the Roo Code sidebar.
- Click on the Prompts tab.
- Here, you can add or edit custom prompts for different modes.
- Save your changes.
- Roo Code will use these prompts when generating code.
Resources
Lab 01 - Basics (Python)
MCP Basics with Python¶
from mcp.server.fastmcp import FastMCP
# Create server instance
mcp = FastMCP("kagent-mcp-server")
@mcp.tool()
def hello(name: str) -> str:
"""Returns a friendly greeting message"""
return f"Hello, {name}! Welcome to K-Agent Labs."
@mcp.tool()
def add(a: float, b: float) -> str:
"""Adds two numbers together"""
result = a + b
return f"The sum of {a} and {b} is {result}"
def main():
# Initialize and run the server
mcp.run()
if __name__ == "__main__":
main()
MCP Server Example (TS)¶
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
/**
* K-Agent MCP Server
* A simple Model Context Protocol server with example tools
*/
// Create server instance
const server = new Server(
{
name: "kagent-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "hello",
description: "Returns a friendly greeting message",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name to greet",
},
},
required: ["name"],
},
},
{
name: "add",
description: "Adds two numbers together",
inputSchema: {
type: "object",
properties: {
a: {
type: "number",
description: "First number",
},
b: {
type: "number",
description: "Second number",
},
},
required: ["a", "b"],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "hello": {
const nameArg = args?.name as string;
return {
content: [
{
type: "text",
text: `Hello, ${nameArg}! Welcome to K-Agent Labs.`,
},
],
};
}
case "add": {
const a = args?.a as number;
const b = args?.b as number;
const result = a + b;
return {
content: [
{
type: "text",
text: `The sum of ${a} and ${b} is ${result}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("K-Agent MCP Server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
Lab 01 - Advanced (Python)
MCP Basics with Python¶
from mcp.server.fastmcp import FastMCP
from starlette.responses import Response, JSONResponse, StreamingResponse
from starlette.requests import Request
import types
import httpx
import json
import asyncio
import time
import inspect
from typing import Any, Dict, List
# Ollama configuration
OLLAMA_BASE_URL = "http://localhost:11434"
DEFAULT_MODEL = "codestral:latest"
# Tool execution tracking
TOOL_EXECUTIONS = {}
EXECUTION_COUNTER = 0
mcp = FastMCP("kagent-mcp-server", port=8889)
# Backwards-compat shim: some inspector tooling (fastmcp helpers)
# expect a `_list_tools_mcp` coroutine on the server instance. Provide
# a thin wrapper that forwards to the FastMCP `list_tools` implementation.
async def _list_tools_mcp(self):
return await self.list_tools()
# Bind the method to the instance
# MCP need to access it as an instance method later
# This will return list of ToolMetadata objects
mcp._list_tools_mcp = types.MethodType(_list_tools_mcp, mcp)
# Common CORS headers used by the inspector (browser-based)
HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Proxy-Token",
}
# Helper functions for tool execution
def get_tool_function(tool_name: str):
"""Get the actual function for a tool by name"""
# Build a mapping of tool names to functions
tool_map = {
'hello': hello,
'add': add,
'ollama_generate': ollama_generate,
'ollama_chat': ollama_chat,
'ollama_list_models': ollama_list_models,
'code_review_prompt': code_review_prompt,
'debug_prompt': debug_prompt,
}
return tool_map.get(tool_name)
def validate_tool_arguments(tool_func, arguments: Dict[str, Any]) -> tuple[bool, str]:
"""Validate arguments against function signature"""
try:
sig = inspect.signature(tool_func)
params = sig.parameters
# Check required arguments
for param_name, param in params.items():
if param.default == inspect.Parameter.empty and param_name not in arguments:
return False, f"Missing required argument: {param_name}"
# Check for unexpected arguments
for arg_name in arguments:
if arg_name not in params:
return False, f"Unexpected argument: {arg_name}"
return True, "Valid"
except Exception as e:
return False, f"Validation error: {str(e)}"
async def execute_tool(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Execute a tool and return the result"""
global EXECUTION_COUNTER
execution_id = f"exec_{EXECUTION_COUNTER}"
EXECUTION_COUNTER += 1
start_time = time.time()
try:
tool_func = get_tool_function(tool_name)
if not tool_func:
return {
"execution_id": execution_id,
"tool": tool_name,
"success": False,
"error": f"Tool '{tool_name}' not found",
"duration_ms": 0
}
# Validate arguments
valid, message = validate_tool_arguments(tool_func, arguments)
if not valid:
return {
"execution_id": execution_id,
"tool": tool_name,
"success": False,
"error": message,
"duration_ms": 0
}
# Execute the tool
if inspect.iscoroutinefunction(tool_func):
result = await tool_func(**arguments)
else:
result = tool_func(**arguments)
duration_ms = (time.time() - start_time) * 1000
execution_record = {
"execution_id": execution_id,
"tool": tool_name,
"arguments": arguments,
"success": True,
"result": result,
"duration_ms": round(duration_ms, 2),
"timestamp": time.time()
}
TOOL_EXECUTIONS[execution_id] = execution_record
return execution_record
except Exception as e:
duration_ms = (time.time() - start_time) * 1000
execution_record = {
"execution_id": execution_id,
"tool": tool_name,
"arguments": arguments,
"success": False,
"error": str(e),
"duration_ms": round(duration_ms, 2),
"timestamp": time.time()
}
TOOL_EXECUTIONS[execution_id] = execution_record
return execution_record
# Example tool definitions
# These will be automatically registered with the MCP server
@mcp.tool()
def hello(name: str) -> str:
"""Returns a friendly greeting message"""
return f"Hello, {name}! Welcome to K-Agent Labs."
# Example tool that adds two numbers
# Demonstrates handling of numeric inputs and outputs
# This will become MCP tool available at /tools
@mcp.tool()
def add(a: float, b: float) -> str:
"""Adds two numbers together"""
result = a + b
return f"The sum of {a} and {b} is {result}"
# Ollama integration tools
@mcp.tool()
def ollama_generate(prompt: str, model: str = DEFAULT_MODEL, max_tokens: int = 500) -> str:
"""Generate text using Ollama LLM"""
try:
import requests
response = requests.post(
f"{OLLAMA_BASE_URL}/api/generate",
json={
"model": model,
"prompt": prompt,
"stream": False,
"options": {
"num_predict": max_tokens
}
},
timeout=60
)
if response.status_code == 200:
result = response.json()
return result.get("response", "No response from model")
else:
return f"Error: {response.status_code} - {response.text}"
except Exception as e:
return f"Error calling Ollama: {str(e)}"
@mcp.tool()
def ollama_chat(message: str, model: str = DEFAULT_MODEL, system: str = "") -> str:
"""Chat with Ollama LLM using chat API"""
try:
import requests
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": message})
response = requests.post(
f"{OLLAMA_BASE_URL}/api/chat",
json={
"model": model,
"messages": messages,
"stream": False
},
timeout=60
)
if response.status_code == 200:
result = response.json()
return result.get("message", {}).get("content", "No response")
else:
return f"Error: {response.status_code} - {response.text}"
except Exception as e:
return f"Error calling Ollama: {str(e)}"
@mcp.tool()
def ollama_list_models() -> str:
"""List available Ollama models"""
try:
import requests
response = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=5)
if response.status_code == 200:
data = response.json()
models = data.get("models", [])
if not models:
return "No models available"
result = "Available Ollama models:\n"
for model in models:
name = model.get("name", "unknown")
size = model.get("size", 0) // (1024**3) # Convert to GB
result += f"- {name} ({size}GB)\n"
return result
else:
return f"Error: {response.status_code}"
except Exception as e:
return f"Error listing models: {str(e)}"
# Prompts - Reusable prompt templates
@mcp.prompt()
def code_review_prompt(code: str, language: str = "python") -> str:
"""Generate a code review prompt for the given code"""
return f"""Please review this {language} code and provide feedback:
---{language}
{code}
---
Focus on:
- Code quality and best practices
- Potential bugs or issues
- Performance improvements
- Security concerns
"""
@mcp.prompt()
def debug_prompt(error_message: str, code_context: str = "") -> str:
"""Generate a debugging prompt"""
prompt = f"""Help me debug this error:
Error: {error_message}
"""
if code_context:
prompt += f"\n\nCode context:\n---\n{code_context}\n---"
return prompt
# Resource to return the source code of this server
# Useful for inspection and learning purposes
@mcp.resource("mcp://code")
def get_code() -> str:
"""Returns the source code of this server"""
with open(__file__, "r") as f:
return f.read()
@mcp.resource("mcp://server-info")
def get_server_info() -> str:
"""Returns information about this MCP server"""
return """K-Agent MCP Server
Version: 0.1.0
Capabilities:
- Tools: hello, add
- Prompts: code_review_prompt, debug_prompt
- Resources: code, server-info
- Sampling: LLM sampling support
- Roots: File system access
"""
@mcp.custom_route("/", methods=["GET", "OPTIONS"])
async def root_health_check(request: Request) -> Response:
return Response("MCP Server Running", status_code=200, headers=HEADERS)
# Health check endpoints
# MCP manifest endpoint, negotiation, and tools listing
@mcp.custom_route("/health", methods=["GET", "OPTIONS"])
async def health_check(request: Request) -> Response:
return Response("MCP Server Running", status_code=200, headers=HEADERS)
# MCP Manifest endpoint as per MCP specification
# Provides metadata about the MCP server
@mcp.custom_route("/.well-known/mcp", methods=["GET", "OPTIONS"])
async def mcp_manifest(request: Request) -> JSONResponse:
host = request.headers.get("host", "localhost:8889")
scheme = request.url.scheme or "http"
base = f"{scheme}://{host}"
manifest = {
"name": "kagent-mcp-server",
"version": "0.1.0",
"base_url": base,
"transport": "streamable-http",
"capabilities": {
"tools": True,
"prompts": True,
"resources": True,
"sampling": True,
"roots": True,
"logging": True
},
"endpoints": {
"manifest": "/.well-known/mcp",
"health": "/health",
"ping": "/ping",
"root": "/",
"negotiate": "/negotiate",
"metadata": "/metadata",
"events": "/mcp",
"tools": "/tools",
"tools_execute": "/tools/execute",
"tools_batch": "/tools/batch",
"tools_stream": "/tools/stream",
"tools_history": "/tools/history",
"prompts": "/prompts",
"resources": "/resources",
"sampling": "/sampling",
"roots": "/roots",
"ollama_status": "/ollama/status",
},
}
return JSONResponse(manifest, headers=HEADERS)
# MCP Negotiation endpoint
# Clients use this to negotiate connection parameters
# Supports token-based authentication
@mcp.custom_route("/negotiate", methods=["GET", "POST", "OPTIONS"])
async def negotiate(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
host = request.headers.get("host", "localhost:8889")
scheme = request.url.scheme or "http"
mcp_url = f"{scheme}://{host}/mcp"
# Accept proxy token from query param, X-Proxy-Token header, or Authorization bearer
token = request.query_params.get("token") or request.headers.get("x-proxy-token") or request.headers.get("X-Proxy-Token")
auth = request.headers.get("authorization") or request.headers.get("Authorization")
if not token and auth and auth.lower().startswith("bearer "):
token = auth.split(None, 1)[1]
response = {
"transport": "streamable-http",
"url": mcp_url,
}
# Include token in response if provided
if token:
response["proxy_token"] = token
else:
# When no token is provided, explicitly indicate no auth is required
# This helps the Inspector understand it can connect without authentication
response["requiresAuth"] = False
return JSONResponse(response, headers=HEADERS)
# Tools listing endpoint
# Returns metadata about all registered tools
# Used by inspectors to discover available tools
@mcp.custom_route("/tools", methods=["POST", "GET", "OPTIONS"])
async def tools_list(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
# Use the server's list_tools implementation so the inspector sees
# the canonical, generated tool metadata instead of a hand-written list.
tools = await mcp._list_tools_mcp()
serializable = []
for t in tools:
try:
serializable.append(t.model_dump())
except Exception:
try:
serializable.append(t.dict())
except Exception:
serializable.append(str(t))
return JSONResponse({"tools": serializable}, headers=HEADERS)
# Tool execution endpoint - Execute a single tool
@mcp.custom_route("/tools/execute", methods=["POST", "OPTIONS"])
async def tool_execute(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
try:
body = await request.json()
tool_name = body.get("tool")
arguments = body.get("arguments", {})
if not tool_name:
return JSONResponse({
"success": False,
"error": "Missing 'tool' field"
}, headers=HEADERS, status_code=400)
result = await execute_tool(tool_name, arguments)
return JSONResponse(result, headers=HEADERS)
except Exception as e:
return JSONResponse({
"success": False,
"error": f"Execution failed: {str(e)}"
}, headers=HEADERS, status_code=500)
# Batch tool execution endpoint
@mcp.custom_route("/tools/batch", methods=["POST", "OPTIONS"])
async def tool_batch_execute(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
try:
body = await request.json()
calls = body.get("calls", [])
if not calls:
return JSONResponse({
"success": False,
"error": "Missing 'calls' array"
}, headers=HEADERS, status_code=400)
results = []
for call in calls:
tool_name = call.get("tool")
arguments = call.get("arguments", {})
if tool_name:
result = await execute_tool(tool_name, arguments)
results.append(result)
else:
results.append({
"success": False,
"error": "Missing tool name in call"
})
return JSONResponse({
"success": True,
"results": results,
"total": len(results)
}, headers=HEADERS)
except Exception as e:
return JSONResponse({
"success": False,
"error": f"Batch execution failed: {str(e)}"
}, headers=HEADERS, status_code=500)
# Tool execution history endpoint
@mcp.custom_route("/tools/history", methods=["GET", "OPTIONS"])
async def tool_history(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
limit = int(request.query_params.get("limit", 10))
# Get recent executions
executions = list(TOOL_EXECUTIONS.values())
executions.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
return JSONResponse({
"executions": executions[:limit],
"total": len(executions)
}, headers=HEADERS)
# Get specific execution details
@mcp.custom_route("/tools/execution/{execution_id}", methods=["GET", "OPTIONS"])
async def tool_execution_detail(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
execution_id = request.path_params.get("execution_id")
if execution_id in TOOL_EXECUTIONS:
return JSONResponse(TOOL_EXECUTIONS[execution_id], headers=HEADERS)
else:
return JSONResponse({
"error": f"Execution '{execution_id}' not found"
}, headers=HEADERS, status_code=404)
# Streaming tool execution
@mcp.custom_route("/tools/stream", methods=["POST", "OPTIONS"])
async def tool_stream_execute(request: Request):
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
try:
body = await request.json()
tool_name = body.get("tool")
arguments = body.get("arguments", {})
async def generate_stream():
# Send start event
yield json.dumps({
"event": "start",
"tool": tool_name,
"timestamp": time.time()
}) + "\n"
# Execute tool
result = await execute_tool(tool_name, arguments)
# Send result event
yield json.dumps({
"event": "result",
"data": result
}) + "\n"
# Send end event
yield json.dumps({
"event": "end",
"timestamp": time.time()
}) + "\n"
return StreamingResponse(
generate_stream(),
media_type="application/x-ndjson",
headers=HEADERS
)
except Exception as e:
return JSONResponse({
"success": False,
"error": f"Stream execution failed: {str(e)}"
}, headers=HEADERS, status_code=500)
# Prompts listing endpoint
@mcp.custom_route("/prompts", methods=["POST", "GET", "OPTIONS"])
async def prompts_list(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
prompts = [
{
"name": "code_review_prompt",
"description": "Generate a code review prompt for the given code",
"arguments": [
{"name": "code", "description": "The code to review", "required": True},
{"name": "language", "description": "Programming language", "required": False}
]
},
{
"name": "debug_prompt",
"description": "Generate a debugging prompt",
"arguments": [
{"name": "error_message", "description": "The error message", "required": True},
{"name": "code_context", "description": "Relevant code context", "required": False}
]
}
]
return JSONResponse({"prompts": prompts}, headers=HEADERS)
# Resources listing endpoint
@mcp.custom_route("/resources", methods=["POST", "GET", "OPTIONS"])
async def resources_list(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
resources = [
{
"uri": "mcp://code",
"name": "Server Source Code",
"description": "Returns the source code of this server",
"mimeType": "text/plain"
},
{
"uri": "mcp://server-info",
"name": "Server Information",
"description": "Returns information about this MCP server",
"mimeType": "text/plain"
}
]
return JSONResponse({"resources": resources}, headers=HEADERS)
# Ping endpoint for connection health checks
@mcp.custom_route("/ping", methods=["POST", "GET", "OPTIONS"])
async def ping(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
return JSONResponse({
"status": "ok",
"timestamp": __import__('time').time(),
"server": "kagent-mcp-server"
}, headers=HEADERS)
# Sampling endpoint - LLM sampling capability using Ollama
@mcp.custom_route("/sampling", methods=["POST", "OPTIONS"])
async def sampling(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
body = await request.json() if request.method == "POST" else {}
prompt = body.get("prompt", "")
max_tokens = body.get("maxTokens", 100)
model = body.get("model", DEFAULT_MODEL)
try:
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"{OLLAMA_BASE_URL}/api/generate",
json={
"model": model,
"prompt": prompt,
"stream": False,
"options": {
"num_predict": max_tokens
}
}
)
if response.status_code == 200:
result = response.json()
return JSONResponse({
"completion": result.get("response", ""),
"stopReason": "endTurn" if result.get("done") else "length",
"model": model,
"context": result.get("context", [])
}, headers=HEADERS)
else:
return JSONResponse({
"error": f"Ollama error: {response.status_code}",
"completion": "",
"stopReason": "error",
"model": model
}, headers=HEADERS, status_code=500)
except Exception as e:
return JSONResponse({
"error": f"Failed to connect to Ollama: {str(e)}",
"completion": "",
"stopReason": "error",
"model": model
}, headers=HEADERS, status_code=500)
# Roots endpoint - File system roots
@mcp.custom_route("/roots", methods=["POST", "GET", "OPTIONS"])
async def roots_list(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
import os
roots = [
{
"uri": f"file://{os.getcwd()}",
"name": "Current Directory"
}
]
return JSONResponse({"roots": roots}, headers=HEADERS)
# Ollama status endpoint
@mcp.custom_route("/ollama/status", methods=["GET", "OPTIONS"])
async def ollama_status(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
if response.status_code == 200:
data = response.json()
models = data.get("models", [])
return JSONResponse({
"status": "connected",
"url": OLLAMA_BASE_URL,
"models_count": len(models),
"models": [{
"name": m.get("name"),
"size": m.get("size"),
"modified": m.get("modified_at")
} for m in models],
"default_model": DEFAULT_MODEL
}, headers=HEADERS)
else:
return JSONResponse({
"status": "error",
"url": OLLAMA_BASE_URL,
"error": f"HTTP {response.status_code}"
}, headers=HEADERS)
except Exception as e:
return JSONResponse({
"status": "disconnected",
"url": OLLAMA_BASE_URL,
"error": str(e)
}, headers=HEADERS)
# Server metadata endpoint
@mcp.custom_route("/metadata", methods=["GET", "OPTIONS"])
async def metadata(request: Request) -> JSONResponse:
# Handle OPTIONS preflight
if request.method == "OPTIONS":
return JSONResponse({}, headers=HEADERS)
return JSONResponse({
"serverInfo": {
"name": "kagent-mcp-server",
"version": "0.1.0",
"protocolVersion": "2024-11-05"
},
"capabilities": {
"tools": {"listChanged": False},
"prompts": {"listChanged": False},
"resources": {"subscribe": False, "listChanged": False},
"logging": {},
"sampling": {},
"roots": {"listChanged": False}
}
}, headers=HEADERS)
# Add CORS middleware to handle preflight requests for all endpoints
@mcp.custom_route("/mcp", methods=["OPTIONS"])
async def mcp_options(request: Request) -> Response:
"""Handle CORS preflight for the MCP endpoint"""
return Response(status_code=200, headers=HEADERS)
# Main entry point to run the MCP server
def main():
# Start the MCP server with streamable-http transport
# Mounted at /mcp path
# This will listen on port 8889 as configured above
mcp.run(transport="streamable-http", mount_path="/mcp")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 1: Imports
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
)
import sys
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 1: Imports
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
)
import sys
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
"""
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 1: Imports
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
)
import sys
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 4: Register Tools
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
)
import sys
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
def register_tools(self):
"""Register all available tools with the MCP server."""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""
Return the list of available tools.
This is called when clients want to discover what tools are available.
"""
return [
Tool(
name="calculate",
description="Perform mathematical operations (add, subtract, multiply, divide)",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
),
Tool(
name="store_data",
description="Store a key-value pair in the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store"
},
"value": {
"type": "string",
"description": "The value to store"
}
},
"required": ["key", "value"]
}
),
Tool(
name="retrieve_data",
description="Retrieve a value from the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to retrieve"
}
},
"required": ["key"]
}
),
Tool(
name="echo",
description="Echo back the input text",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to echo back"
}
},
"required": ["text"]
}
)
]
print("Tools registered: calculate, store_data, retrieve_data, echo")
async def run(self):
"""Run the MCP server."""
# Initialize everything
self.register_tools()
# Connect to stdio
async with stdio_server() as (read_stream, write_stream):
print("Server running on stdio...")
await self.server.run(
read_stream,
write_stream,
self.server.create_initialization_options()
)
async def main():
"""Main entry point."""
server = CompleteMCPServer()
await server.run()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Server stopped by user")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 5: Register Tool Handlers
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
)
import sys
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
def register_tools(self):
"""Register all available tools with the MCP server."""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""
Return the list of available tools.
This is called when clients want to discover what tools are available.
"""
return [
Tool(
name="calculate",
description="Perform mathematical operations (add, subtract, multiply, divide)",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
),
Tool(
name="store_data",
description="Store a key-value pair in the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store"
},
"value": {
"type": "string",
"description": "The value to store"
}
},
"required": ["key", "value"]
}
),
Tool(
name="retrieve_data",
description="Retrieve a value from the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to retrieve"
}
},
"required": ["key"]
}
),
Tool(
name="echo",
description="Echo back the input text",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to echo back"
}
},
"required": ["text"]
}
)
]
print("Tools registered: calculate, store_data, retrieve_data, echo")
def register_tool_handlers(self):
"""Implement the actual logic for each tool."""
@self.server.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""
Handle tool execution requests.
This is called when a client wants to execute a tool.
"""
if name == "calculate":
operation = arguments.get("operation")
a = arguments.get("a")
b = arguments.get("b")
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
return [TextContent(
type="text",
text="Error: Cannot divide by zero"
)]
result = a / b
else:
return [TextContent(
type="text",
text=f"Error: Unknown operation '{operation}'"
)]
return [TextContent(
type="text",
text=f"Result: {a} {operation} {b} = {result}"
)]
elif name == "store_data":
key = arguments.get("key")
value = arguments.get("value")
self.data_store[key] = value
return [TextContent(
type="text",
text=f"Stored: {key} = {value}"
)]
elif name == "retrieve_data":
key = arguments.get("key")
value = self.data_store.get(key)
if value is None:
return [TextContent(
type="text",
text=f"Error: Key '{key}' not found"
)]
return [TextContent(
type="text",
text=f"Retrieved: {key} = {value}"
)]
elif name == "echo":
text = arguments.get("text")
return [TextContent(
type="text",
text=f"Echo: {text}"
)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown tool '{name}'"
)]
print("Tool handlers implemented")
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 6: Register Resources
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
)
import sys
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
def register_tools(self):
"""Register all available tools with the MCP server."""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""
Return the list of available tools.
This is called when clients want to discover what tools are available.
"""
return [
Tool(
name="calculate",
description="Perform mathematical operations (add, subtract, multiply, divide)",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
),
Tool(
name="store_data",
description="Store a key-value pair in the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store"
},
"value": {
"type": "string",
"description": "The value to store"
}
},
"required": ["key", "value"]
}
),
Tool(
name="retrieve_data",
description="Retrieve a value from the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to retrieve"
}
},
"required": ["key"]
}
),
Tool(
name="echo",
description="Echo back the input text",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to echo back"
}
},
"required": ["text"]
}
)
]
print("Tools registered: calculate, store_data, retrieve_data, echo")
def register_tool_handlers(self):
"""Implement the actual logic for each tool."""
@self.server.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""
Handle tool execution requests.
This is called when a client wants to execute a tool.
"""
if name == "calculate":
operation = arguments.get("operation")
a = arguments.get("a")
b = arguments.get("b")
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
return [TextContent(
type="text",
text="Error: Cannot divide by zero"
)]
result = a / b
else:
return [TextContent(
type="text",
text=f"Error: Unknown operation '{operation}'"
)]
return [TextContent(
type="text",
text=f"Result: {a} {operation} {b} = {result}"
)]
elif name == "store_data":
key = arguments.get("key")
value = arguments.get("value")
self.data_store[key] = value
return [TextContent(
type="text",
text=f"Stored: {key} = {value}"
)]
elif name == "retrieve_data":
key = arguments.get("key")
value = self.data_store.get(key)
if value is None:
return [TextContent(
type="text",
text=f"Error: Key '{key}' not found"
)]
return [TextContent(
type="text",
text=f"Retrieved: {key} = {value}"
)]
elif name == "echo":
text = arguments.get("text")
return [TextContent(
type="text",
text=f"Echo: {text}"
)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown tool '{name}'"
)]
print("Tool handlers implemented")
def register_resources(self):
"""Register resources that clients can access."""
@self.server.list_resources()
async def list_resources() -> list[Resource]:
"""
Return the list of available resources.
This is called when clients want to discover what resources are available.
"""
return [
Resource(
uri="resource://server-info",
name="Server Information",
description="Information about this MCP server",
mimeType="application/json"
),
Resource(
uri="resource://data-store",
name="Data Store",
description="Current contents of the data store",
mimeType="application/json"
),
Resource(
uri="resource://welcome",
name="Welcome Message",
description="Welcome message and server capabilities",
mimeType="text/plain"
)
]
print("Resources registered: server-info, data-store, welcome")
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 7: Register Resource Handlers
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
)
import sys
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
def register_tools(self):
"""Register all available tools with the MCP server."""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""
Return the list of available tools.
This is called when clients want to discover what tools are available.
"""
return [
Tool(
name="calculate",
description="Perform mathematical operations (add, subtract, multiply, divide)",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
),
Tool(
name="store_data",
description="Store a key-value pair in the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store"
},
"value": {
"type": "string",
"description": "The value to store"
}
},
"required": ["key", "value"]
}
),
Tool(
name="retrieve_data",
description="Retrieve a value from the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to retrieve"
}
},
"required": ["key"]
}
),
Tool(
name="echo",
description="Echo back the input text",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to echo back"
}
},
"required": ["text"]
}
)
]
print("Tools registered: calculate, store_data, retrieve_data, echo")
def register_tool_handlers(self):
"""Implement the actual logic for each tool."""
@self.server.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""
Handle tool execution requests.
This is called when a client wants to execute a tool.
"""
if name == "calculate":
operation = arguments.get("operation")
a = arguments.get("a")
b = arguments.get("b")
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
return [TextContent(
type="text",
text="Error: Cannot divide by zero"
)]
result = a / b
else:
return [TextContent(
type="text",
text=f"Error: Unknown operation '{operation}'"
)]
return [TextContent(
type="text",
text=f"Result: {a} {operation} {b} = {result}"
)]
elif name == "store_data":
key = arguments.get("key")
value = arguments.get("value")
self.data_store[key] = value
return [TextContent(
type="text",
text=f"Stored: {key} = {value}"
)]
elif name == "retrieve_data":
key = arguments.get("key")
value = self.data_store.get(key)
if value is None:
return [TextContent(
type="text",
text=f"Error: Key '{key}' not found"
)]
return [TextContent(
type="text",
text=f"Retrieved: {key} = {value}"
)]
elif name == "echo":
text = arguments.get("text")
return [TextContent(
type="text",
text=f"Echo: {text}"
)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown tool '{name}'"
)]
print("Tool handlers implemented")
def register_resources(self):
"""Register resources that clients can access."""
@self.server.list_resources()
async def list_resources() -> list[Resource]:
"""
Return the list of available resources.
This is called when clients want to discover what resources are available.
"""
return [
Resource(
uri="resource://server-info",
name="Server Information",
description="Information about this MCP server",
mimeType="application/json"
),
Resource(
uri="resource://data-store",
name="Data Store",
description="Current contents of the data store",
mimeType="application/json"
),
Resource(
uri="resource://welcome",
name="Welcome Message",
description="Welcome message and server capabilities",
mimeType="text/plain"
)
]
print("Resources registered: server-info, data-store, welcome")
def register_resource_handlers(self):
"""Implement resource retrieval logic."""
@self.server.read_resource()
async def read_resource(uri: str) -> str:
"""
Handle resource read requests.
This is called when a client wants to read a resource.
"""
if uri == "resource://server-info":
info = {
"name": "complete-mcp-server",
"version": "1.0.0",
"description": "A comprehensive MCP server implementation",
"capabilities": {
"tools": 4,
"resources": 3,
"prompts": 2
}
}
return json.dumps(info, indent=2)
elif uri == "resource://data-store":
return json.dumps(self.data_store, indent=2)
elif uri == "resource://welcome":
return """Welcome to the Complete MCP Server!
This server demonstrates all MCP protocol capabilities:
- Tools: Execute operations and computations
- Resources: Access data and information
- Prompts: Get structured prompt templates
Explore the available tools and resources to see what this server can do."""
else:
raise ValueError(f"Unknown resource: {uri}")
print("Resource handlers implemented")
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 8: Register Prompts
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent, ImageContent,
EmbeddedResource,
)
import sys
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
def register_tools(self):
"""Register all available tools with the MCP server."""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""
Return the list of available tools.
This is called when clients want to discover what tools are available.
"""
return [
Tool(
name="calculate",
description="Perform mathematical operations (add, subtract, multiply, divide)",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
),
Tool(
name="store_data",
description="Store a key-value pair in the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store"
},
"value": {
"type": "string",
"description": "The value to store"
}
},
"required": ["key", "value"]
}
),
Tool(
name="retrieve_data",
description="Retrieve a value from the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to retrieve"
}
},
"required": ["key"]
}
),
Tool(
name="echo",
description="Echo back the input text",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to echo back"
}
},
"required": ["text"]
}
)
]
print("Tools registered: calculate, store_data, retrieve_data, echo")
def register_tool_handlers(self):
"""Implement the actual logic for each tool."""
@self.server.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""
Handle tool execution requests.
This is called when a client wants to execute a tool.
"""
if name == "calculate":
operation = arguments.get("operation")
a = arguments.get("a")
b = arguments.get("b")
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
return [TextContent(
type="text",
text="Error: Cannot divide by zero"
)]
result = a / b
else:
return [TextContent(
type="text",
text=f"Error: Unknown operation '{operation}'"
)]
return [TextContent(
type="text",
text=f"Result: {a} {operation} {b} = {result}"
)]
elif name == "store_data":
key = arguments.get("key")
value = arguments.get("value")
self.data_store[key] = value
return [TextContent(
type="text",
text=f"Stored: {key} = {value}"
)]
elif name == "retrieve_data":
key = arguments.get("key")
value = self.data_store.get(key)
if value is None:
return [TextContent(
type="text",
text=f"Error: Key '{key}' not found"
)]
return [TextContent(
type="text",
text=f"Retrieved: {key} = {value}"
)]
elif name == "echo":
text = arguments.get("text")
return [TextContent(
type="text",
text=f"Echo: {text}"
)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown tool '{name}'"
)]
print("Tool handlers implemented")
def register_resources(self):
"""Register resources that clients can access."""
@self.server.list_resources()
async def list_resources() -> list[Resource]:
"""
Return the list of available resources.
This is called when clients want to discover what resources are available.
"""
return [
Resource(
uri="resource://server-info",
name="Server Information",
description="Information about this MCP server",
mimeType="application/json"
),
Resource(
uri="resource://data-store",
name="Data Store",
description="Current contents of the data store",
mimeType="application/json"
),
Resource(
uri="resource://welcome",
name="Welcome Message",
description="Welcome message and server capabilities",
mimeType="text/plain"
)
]
print("Resources registered: server-info, data-store, welcome")
def register_resource_handlers(self):
"""Implement resource retrieval logic."""
@self.server.read_resource()
async def read_resource(uri: str) -> str:
"""
Handle resource read requests.
This is called when a client wants to read a resource.
"""
if uri == "resource://server-info":
info = {
"name": "complete-mcp-server",
"version": "1.0.0",
"description": "A comprehensive MCP server implementation",
"capabilities": {
"tools": 4,
"resources": 3,
"prompts": 2
}
}
return json.dumps(info, indent=2)
elif uri == "resource://data-store":
return json.dumps(self.data_store, indent=2)
elif uri == "resource://welcome":
return """Welcome to the Complete MCP Server!
This server demonstrates all MCP protocol capabilities:
- Tools: Execute operations and computations
- Resources: Access data and information
- Prompts: Get structured prompt templates
Explore the available tools and resources to see what this server can do."""
else:
raise ValueError(f"Unknown resource: {uri}")
print("Resource handlers implemented")
def register_prompts(self):
"""Register prompt templates for clients."""
@self.server.list_prompts()
async def list_prompts() -> list[Prompt]:
"""
Return the list of available prompts.
This is called when clients want to discover what prompts are available.
"""
return [
Prompt(
name="analyze-data",
description="Analyze data stored in the server",
arguments=[
{
"name": "key",
"description": "The key of the data to analyze",
"required": True
}
]
),
Prompt(
name="calculate-scenario",
description="Walk through a calculation scenario",
arguments=[
{
"name": "operation",
"description": "The operation to demonstrate (add, subtract, multiply, divide)",
"required": True
}
]
)
]
print("Prompts registered: analyze-data, calculate-scenario")
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 9: Register Prompt Handlers
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
)
import sys
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
def register_tools(self):
"""Register all available tools with the MCP server."""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""
Return the list of available tools.
This is called when clients want to discover what tools are available.
"""
return [
Tool(
name="calculate",
description="Perform mathematical operations (add, subtract, multiply, divide)",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
),
Tool(
name="store_data",
description="Store a key-value pair in the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store"
},
"value": {
"type": "string",
"description": "The value to store"
}
},
"required": ["key", "value"]
}
),
Tool(
name="retrieve_data",
description="Retrieve a value from the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to retrieve"
}
},
"required": ["key"]
}
),
Tool(
name="echo",
description="Echo back the input text",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to echo back"
}
},
"required": ["text"]
}
)
]
print("Tools registered: calculate, store_data, retrieve_data, echo")
def register_tool_handlers(self):
"""Implement the actual logic for each tool."""
@self.server.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""
Handle tool execution requests.
This is called when a client wants to execute a tool.
"""
if name == "calculate":
operation = arguments.get("operation")
a = arguments.get("a")
b = arguments.get("b")
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
return [TextContent(
type="text",
text="Error: Cannot divide by zero"
)]
result = a / b
else:
return [TextContent(
type="text",
text=f"Error: Unknown operation '{operation}'"
)]
return [TextContent(
type="text",
text=f"Result: {a} {operation} {b} = {result}"
)]
elif name == "store_data":
key = arguments.get("key")
value = arguments.get("value")
self.data_store[key] = value
return [TextContent(
type="text",
text=f"Stored: {key} = {value}"
)]
elif name == "retrieve_data":
key = arguments.get("key")
value = self.data_store.get(key)
if value is None:
return [TextContent(
type="text",
text=f"Error: Key '{key}' not found"
)]
return [TextContent(
type="text",
text=f"Retrieved: {key} = {value}"
)]
elif name == "echo":
text = arguments.get("text")
return [TextContent(
type="text",
text=f"Echo: {text}"
)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown tool '{name}'"
)]
print("Tool handlers implemented")
def register_resources(self):
"""Register resources that clients can access."""
@self.server.list_resources()
async def list_resources() -> list[Resource]:
"""
Return the list of available resources.
This is called when clients want to discover what resources are available.
"""
return [
Resource(
uri="resource://server-info",
name="Server Information",
description="Information about this MCP server",
mimeType="application/json"
),
Resource(
uri="resource://data-store",
name="Data Store",
description="Current contents of the data store",
mimeType="application/json"
),
Resource(
uri="resource://welcome",
name="Welcome Message",
description="Welcome message and server capabilities",
mimeType="text/plain"
)
]
print("Resources registered: server-info, data-store, welcome")
def register_resource_handlers(self):
"""Implement resource retrieval logic."""
@self.server.read_resource()
async def read_resource(uri: str) -> str:
"""
Handle resource read requests.
This is called when a client wants to read a resource.
"""
if uri == "resource://server-info":
info = {
"name": "complete-mcp-server",
"version": "1.0.0",
"description": "A comprehensive MCP server implementation",
"capabilities": {
"tools": 4,
"resources": 3,
"prompts": 2
}
}
return json.dumps(info, indent=2)
elif uri == "resource://data-store":
return json.dumps(self.data_store, indent=2)
elif uri == "resource://welcome":
return """Welcome to the Complete MCP Server!
This server demonstrates all MCP protocol capabilities:
- Tools: Execute operations and computations
- Resources: Access data and information
- Prompts: Get structured prompt templates
Explore the available tools and resources to see what this server can do."""
else:
raise ValueError(f"Unknown resource: {uri}")
print("Resource handlers implemented")
def register_prompts(self):
"""Register prompt templates for clients."""
@self.server.list_prompts()
async def list_prompts() -> list[Prompt]:
"""
Return the list of available prompts.
This is called when clients want to discover what prompts are available.
"""
return [
Prompt(
name="analyze-data",
description="Analyze data stored in the server",
arguments=[
{
"name": "key",
"description": "The key of the data to analyze",
"required": True
}
]
),
Prompt(
name="calculate-scenario",
description="Walk through a calculation scenario",
arguments=[
{
"name": "operation",
"description": "The operation to demonstrate (add, subtract, multiply, divide)",
"required": True
}
]
)
]
print("Prompts registered: analyze-data, calculate-scenario")
def register_prompt_handlers(self):
"""Implement prompt generation logic."""
@self.server.get_prompt()
async def get_prompt(name: str, arguments: dict) -> list[TextContent]:
"""
Handle prompt generation requests.
This is called when a client wants to get a prompt.
"""
if name == "analyze-data":
key = arguments.get("key", "unknown")
value = self.data_store.get(key, "not found")
prompt_text = f"""Analyze the following data from the server:
Key: {key}
Value: {value}
Please provide:
1. A description of what this data represents
2. Any patterns or insights you notice
3. Suggestions for how this data could be used
Use the retrieve_data tool if you need to fetch additional context."""
return [TextContent(type="text", text=prompt_text)]
elif name == "calculate-scenario":
operation = arguments.get("operation", "add")
prompt_text = f"""Let's work through a {operation} calculation scenario.
Use the calculate tool with the operation '{operation}'.
For example:
- Choose two numbers (a and b)
- Execute: calculate(operation="{operation}", a=10, b=5)
- Explain the result
This demonstrates how to use computational tools in the MCP server."""
return [TextContent(type="text", text=prompt_text)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown prompt '{name}'"
)]
print("Prompt handlers implemented")
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 10: Lifecycle Handlers
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
)
import sys
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
def register_tools(self):
"""Register all available tools with the MCP server."""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""
Return the list of available tools.
This is called when clients want to discover what tools are available.
"""
return [
Tool(
name="calculate",
description="Perform mathematical operations (add, subtract, multiply, divide)",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
),
Tool(
name="store_data",
description="Store a key-value pair in the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store"
},
"value": {
"type": "string",
"description": "The value to store"
}
},
"required": ["key", "value"]
}
),
Tool(
name="retrieve_data",
description="Retrieve a value from the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to retrieve"
}
},
"required": ["key"]
}
),
Tool(
name="echo",
description="Echo back the input text",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to echo back"
}
},
"required": ["text"]
}
)
]
print("Tools registered: calculate, store_data, retrieve_data, echo")
def register_tool_handlers(self):
"""Implement the actual logic for each tool."""
@self.server.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""
Handle tool execution requests.
This is called when a client wants to execute a tool.
"""
if name == "calculate":
operation = arguments.get("operation")
a = arguments.get("a")
b = arguments.get("b")
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
return [TextContent(
type="text",
text="Error: Cannot divide by zero"
)]
result = a / b
else:
return [TextContent(
type="text",
text=f"Error: Unknown operation '{operation}'"
)]
return [TextContent(
type="text",
text=f"Result: {a} {operation} {b} = {result}"
)]
elif name == "store_data":
key = arguments.get("key")
value = arguments.get("value")
self.data_store[key] = value
return [TextContent(
type="text",
text=f"Stored: {key} = {value}"
)]
elif name == "retrieve_data":
key = arguments.get("key")
value = self.data_store.get(key)
if value is None:
return [TextContent(
type="text",
text=f"Error: Key '{key}' not found"
)]
return [TextContent(
type="text",
text=f"Retrieved: {key} = {value}"
)]
elif name == "echo":
text = arguments.get("text")
return [TextContent(
type="text",
text=f"Echo: {text}"
)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown tool '{name}'"
)]
print("Tool handlers implemented")
def register_resources(self):
"""Register resources that clients can access."""
@self.server.list_resources()
async def list_resources() -> list[Resource]:
"""
Return the list of available resources.
This is called when clients want to discover what resources are available.
"""
return [
Resource(
uri="resource://server-info",
name="Server Information",
description="Information about this MCP server",
mimeType="application/json"
),
Resource(
uri="resource://data-store",
name="Data Store",
description="Current contents of the data store",
mimeType="application/json"
),
Resource(
uri="resource://welcome",
name="Welcome Message",
description="Welcome message and server capabilities",
mimeType="text/plain"
)
]
print("Resources registered: server-info, data-store, welcome")
def register_resource_handlers(self):
"""Implement resource retrieval logic."""
@self.server.read_resource()
async def read_resource(uri: str) -> str:
"""
Handle resource read requests.
This is called when a client wants to read a resource.
"""
if uri == "resource://server-info":
info = {
"name": "complete-mcp-server",
"version": "1.0.0",
"description": "A comprehensive MCP server implementation",
"capabilities": {
"tools": 4,
"resources": 3,
"prompts": 2
}
}
return json.dumps(info, indent=2)
elif uri == "resource://data-store":
return json.dumps(self.data_store, indent=2)
elif uri == "resource://welcome":
return """Welcome to the Complete MCP Server!
This server demonstrates all MCP protocol capabilities:
- Tools: Execute operations and computations
- Resources: Access data and information
- Prompts: Get structured prompt templates
Explore the available tools and resources to see what this server can do."""
else:
raise ValueError(f"Unknown resource: {uri}")
print("Resource handlers implemented")
def register_prompts(self):
"""Register prompt templates for clients."""
@self.server.list_prompts()
async def list_prompts() -> list[Prompt]:
"""
Return the list of available prompts.
This is called when clients want to discover what prompts are available.
"""
return [
Prompt(
name="analyze-data",
description="Analyze data stored in the server",
arguments=[
{
"name": "key",
"description": "The key of the data to analyze",
"required": True
}
]
),
Prompt(
name="calculate-scenario",
description="Walk through a calculation scenario",
arguments=[
{
"name": "operation",
"description": "The operation to demonstrate (add, subtract, multiply, divide)",
"required": True
}
]
)
]
print("Prompts registered: analyze-data, calculate-scenario")
def register_prompt_handlers(self):
"""Implement prompt generation logic."""
@self.server.get_prompt()
async def get_prompt(name: str, arguments: dict) -> list[TextContent]:
"""
Handle prompt generation requests.
This is called when a client wants to get a prompt.
"""
if name == "analyze-data":
key = arguments.get("key", "unknown")
value = self.data_store.get(key, "not found")
prompt_text = f"""Analyze the following data from the server:
Key: {key}
Value: {value}
Please provide:
1. A description of what this data represents
2. Any patterns or insights you notice
3. Suggestions for how this data could be used
Use the retrieve_data tool if you need to fetch additional context."""
return [TextContent(type="text", text=prompt_text)]
elif name == "calculate-scenario":
operation = arguments.get("operation", "add")
prompt_text = f"""Let's work through a {operation} calculation scenario.
Use the calculate tool with the operation '{operation}'.
For example:
- Choose two numbers (a and b)
- Execute: calculate(operation="{operation}", a=10, b=5)
- Explain the result
This demonstrates how to use computational tools in the MCP server."""
return [TextContent(type="text", text=prompt_text)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown prompt '{name}'"
)]
print("Prompt handlers implemented")
def setup_lifecycle_handlers(self):
"""Setup lifecycle management (conceptual for MCP)."""
# Note: MCP servers typically don't have explicit lifecycle hooks
# This is a conceptual method showing where such logic would go
print("Lifecycle management configured")
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 11: Main Execution
"""
import asyncio
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
GetPromptResult,
PromptMessage,
)
import sys
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
def register_tools(self):
"""Register all available tools with the MCP server."""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""
Return the list of available tools.
This is called when clients want to discover what tools are available.
"""
return [
Tool(
name="calculate",
description="Perform mathematical operations (add, subtract, multiply, divide)",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
),
Tool(
name="store_data",
description="Store a key-value pair in the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store"
},
"value": {
"type": "string",
"description": "The value to store"
}
},
"required": ["key", "value"]
}
),
Tool(
name="retrieve_data",
description="Retrieve a value from the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to retrieve"
}
},
"required": ["key"]
}
),
Tool(
name="echo",
description="Echo back the input text",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to echo back"
}
},
"required": ["text"]
}
)
]
print("Tools registered: calculate, store_data, retrieve_data, echo")
def register_tool_handlers(self):
"""Implement the actual logic for each tool."""
@self.server.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""
Handle tool execution requests.
This is called when a client wants to execute a tool.
"""
if name == "calculate":
operation = arguments.get("operation")
a = arguments.get("a")
b = arguments.get("b")
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
return [TextContent(
type="text",
text="Error: Cannot divide by zero"
)]
result = a / b
else:
return [TextContent(
type="text",
text=f"Error: Unknown operation '{operation}'"
)]
return [TextContent(
type="text",
text=f"Result: {a} {operation} {b} = {result}"
)]
elif name == "store_data":
key = arguments.get("key")
value = arguments.get("value")
self.data_store[key] = value
return [TextContent(
type="text",
text=f"Stored: {key} = {value}"
)]
elif name == "retrieve_data":
key = arguments.get("key")
value = self.data_store.get(key)
if value is None:
return [TextContent(
type="text",
text=f"Error: Key '{key}' not found"
)]
return [TextContent(
type="text",
text=f"Retrieved: {key} = {value}"
)]
elif name == "echo":
text = arguments.get("text")
return [TextContent(
type="text",
text=f"Echo: {text}"
)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown tool '{name}'"
)]
print("Tool handlers implemented")
def register_resources(self):
"""Register resources that clients can access."""
@self.server.list_resources()
async def list_resources() -> list[Resource]:
"""
Return the list of available resources.
This is called when clients want to discover what resources are available.
"""
return [
Resource(
uri="resource://server-info",
name="Server Information",
description="Information about this MCP server",
mimeType="application/json"
),
Resource(
uri="resource://data-store",
name="Data Store",
description="Current contents of the data store",
mimeType="application/json"
),
Resource(
uri="resource://welcome",
name="Welcome Message",
description="Welcome message and server capabilities",
mimeType="text/plain"
)
]
print("Resources registered: server-info, data-store, welcome")
def register_resource_handlers(self):
"""Implement resource retrieval logic."""
@self.server.read_resource()
async def read_resource(uri: str) -> str:
"""
Handle resource read requests.
This is called when a client wants to read a resource.
"""
uri = str(uri) # Ensure uri is a string
if uri == "resource://server-info":
info = {
"name": "complete-mcp-server",
"version": "1.0.0",
"description": "A comprehensive MCP server implementation",
"capabilities": {
"tools": 4,
"resources": 3,
"prompts": 2
}
}
return json.dumps(info, indent=2)
elif uri == "resource://data-store":
return json.dumps(self.data_store, indent=2)
elif uri == "resource://welcome":
return """Welcome to the Complete MCP Server!
This server demonstrates all MCP protocol capabilities:
- Tools: Execute operations and computations
- Resources: Access data and information
- Prompts: Get structured prompt templates
Explore the available tools and resources to see what this server can do."""
else:
raise ValueError(f"Unknown resource: {uri}")
print("Resource handlers implemented")
def register_prompts(self):
"""Register prompt templates for clients."""
@self.server.list_prompts()
async def list_prompts() -> list[Prompt]:
"""
Return the list of available prompts.
This is called when clients want to discover what prompts are available.
"""
return [
Prompt(
name="analyze-data",
description="Analyze data stored in the server",
arguments=[
{
"name": "key",
"description": "The key of the data to analyze",
"required": True
}
]
),
Prompt(
name="calculate-scenario",
description="Walk through a calculation scenario",
arguments=[
{
"name": "operation",
"description": "The operation to demonstrate (add, subtract, multiply, divide)",
"required": True
}
]
)
]
print("Prompts registered: analyze-data, calculate-scenario")
def register_prompt_handlers(self):
"""Implement prompt generation logic."""
@self.server.get_prompt()
async def get_prompt(name: str, arguments: dict) -> GetPromptResult:
"""
Handle prompt generation requests.
This is called when a client wants to get a prompt.
"""
if name == "analyze-data":
key = arguments.get("key", "unknown")
value = self.data_store.get(key, "not found")
prompt_text = f"""Analyze the following data from the server:
Key: {key}
Value: {value}
Please provide:
1. A description of what this data represents
2. Any patterns or insights you notice
3. Suggestions for how this data could be used
Use the retrieve_data tool if you need to fetch additional context."""
return GetPromptResult(
messages=[
PromptMessage(
role="user",
content=TextContent(type="text", text=prompt_text)
)
]
)
elif name == "calculate-scenario":
operation = arguments.get("operation", "add")
prompt_text = f"""Let's work through a {operation} calculation scenario.
Use the calculate tool with the operation '{operation}'.
For example:
- Choose two numbers (a and b)
- Execute: calculate(operation="{operation}", a=10, b=5)
- Explain the result
This demonstrates how to use computational tools in the MCP server."""
return GetPromptResult(
messages=[
PromptMessage(
role="user",
content=TextContent(type="text", text=prompt_text)
)
]
)
else:
# Helper to create error message in correct format
return GetPromptResult(
messages=[
PromptMessage(
role="user",
content=TextContent(
type="text",
text=f"Error: Unknown prompt '{name}'"
)
)
]
)
print("Prompt handlers implemented")
def setup_lifecycle_handlers(self):
"""Setup lifecycle management (conceptual for MCP)."""
# Note: MCP servers typically don't have explicit lifecycle hooks
# This is a conceptual method showing where such logic would go
print("Lifecycle management configured")
async def run(self):
"""Run the MCP server."""
# Initialize everything
self.register_tools()
self.register_tool_handlers()
self.register_resources()
self.register_resource_handlers()
self.register_prompts()
self.register_prompt_handlers()
self.setup_lifecycle_handlers()
# Connect to stdio
async with stdio_server() as (read_stream, write_stream):
print("Server running on stdio...")
await self.server.run(
read_stream,
write_stream,
self.server.create_initialization_options()
)
async def main():
"""Main entry point."""
server = CompleteMCPServer()
await server.run()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Server stopped by user")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 12: RAG (Retrieval Augmented Generation)
"""
import asyncio
import csv
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
GetPromptResult,
PromptMessage,
)
import sys
# Initialize an in-memory users
users = []
def load_users(csv_file_path: str):
"""Load users from a CSV file."""
global users
users = []
try:
with open(csv_file_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for i, row in enumerate(reader):
# Assuming CSV has 'content' and 'id' columns or similar
# Adjust column names as needed
content = row.get('content') or row.get('text') or list(row.values())[0]
doc_id = row.get('id') or f"doc_{i}"
if content:
users.append({"id": doc_id, "content": content})
print(f"Loaded {len(users)} documents into users.")
except Exception as e:
print(f"Error loading users: {e}")
# Load the users
# Make sure you have a 'users.csv' file in the same directory
# Format: id,content
load_users("users.csv")
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
- RAG capabilities
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
def register_tools(self):
"""Register all available tools with the MCP server."""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""
Return the list of available tools.
This is called when clients want to discover what tools are available.
"""
return [
Tool(
name="calculate",
description="Perform mathematical operations (add, subtract, multiply, divide)",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
),
Tool(
name="store_data",
description="Store a key-value pair in the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store"
},
"value": {
"type": "string",
"description": "The value to store"
}
},
"required": ["key", "value"]
}
),
Tool(
name="retrieve_data",
description="Retrieve a value from the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to retrieve"
}
},
"required": ["key"]
}
),
Tool(
name="echo",
description="Echo back the input text",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to echo back"
}
},
"required": ["text"]
}
),
Tool(
name="filter_users_by_city",
description="Filter and return users who live in a specific city",
inputSchema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city to filter users by"
}
},
"required": ["city"]
}
),
Tool(
name="filter_users_by_age",
description="Filter and return users who are older than the specified minimum age",
inputSchema={
"type": "object",
"properties": {
"min_age": {
"type": "number",
"description": "The minimum age to filter users by"
}
},
"required": ["min_age"]
}
)
]
print("Tools registered: calculate, store_data, retrieve_data, echo, filter_users_by_city, filter_users_by_age")
def register_tool_handlers(self):
"""Implement the actual logic for each tool."""
@self.server.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""
Handle tool execution requests.
This is called when a client wants to execute a tool.
"""
if name == "calculate":
operation = arguments.get("operation")
a = arguments.get("a")
b = arguments.get("b")
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
return [TextContent(
type="text",
text="Error: Cannot divide by zero"
)]
result = a / b
else:
return [TextContent(
type="text",
text=f"Error: Unknown operation '{operation}'"
)]
return [TextContent(
type="text",
text=f"Result: {a} {operation} {b} = {result}"
)]
elif name == "store_data":
key = arguments.get("key")
value = arguments.get("value")
self.data_store[key] = value
return [TextContent(
type="text",
text=f"Stored: {key} = {value}"
)]
elif name == "retrieve_data":
key = arguments.get("key")
value = self.data_store.get(key)
if value is None:
return [TextContent(
type="text",
text=f"Error: Key '{key}' not found"
)]
return [TextContent(
type="text",
text=f"Retrieved: {key} = {value}"
)]
elif name == "echo":
text = arguments.get("text")
return [TextContent(
type="text",
text=f"Echo: {text}"
)]
elif name == "filter_users_by_city":
city = arguments.get("city", "")
filtered_users = []
target_city = city.lower().strip()
for user in users:
# Assuming user dict has 'city' key (loaded from CSV)
u_city = user.get("city", "").lower()
if u_city == target_city:
filtered_users.append(f"User {user.get('id')}: {user.get('content')} (City: {u_city})")
if not filtered_users:
result = f"No users found in {city}."
else:
result = "\n".join(filtered_users)
return [TextContent(type="text", text=result)]
elif name == "filter_users_by_age":
min_age = arguments.get("min_age", 0)
filtered_users = []
for user in users:
# Assuming user dict has 'age' (or 'value') key
u_age = user.get("age", user.get("value", 0))
try:
u_age = int(u_age)
except ValueError:
continue
if u_age > min_age:
filtered_users.append(f"User {user.get('id')}: {user.get('content')} (Age: {u_age})")
if not filtered_users:
result = f"No users found older than {min_age}."
else:
result = "\n".join(filtered_users)
return [TextContent(type="text", text=result)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown tool '{name}'"
)]
print("Tool handlers implemented")
def register_resources(self):
"""Register resources that clients can access."""
@self.server.list_resources()
async def list_resources() -> list[Resource]:
"""
Return the list of available resources.
This is called when clients want to discover what resources are available.
"""
return [
Resource(
uri="resource://server-info",
name="Server Information",
description="Information about this MCP server",
mimeType="application/json"
),
Resource(
uri="resource://data-store",
name="Data Store",
description="Current contents of the data store",
mimeType="application/json"
),
Resource(
uri="resource://welcome",
name="Welcome Message",
description="Welcome message and server capabilities",
mimeType="text/plain"
)
]
print("Resources registered: server-info, data-store, welcome")
def register_resource_handlers(self):
"""Implement resource retrieval logic."""
@self.server.read_resource()
async def read_resource(uri: str) -> str:
"""
Handle resource read requests.
This is called when a client wants to read a resource.
"""
uri = str(uri) # Ensure uri is a string
if uri == "resource://server-info":
info = {
"name": "complete-mcp-server",
"version": "1.0.0",
"description": "A comprehensive MCP server implementation",
"capabilities": {
"tools": 6,
"resources": 3,
"prompts": 2
}
}
return json.dumps(info, indent=2)
elif uri == "resource://data-store":
return json.dumps(self.data_store, indent=2)
elif uri == "resource://welcome":
return """Welcome to the Complete MCP Server!
This server demonstrates all MCP protocol capabilities:
- Tools: Execute operations and computations
- Resources: Access data and information
- Prompts: Get structured prompt templates
- RAG: Retrieval Augmented Generation for user filtering
Explore the available tools and resources to see what this server can do."""
else:
raise ValueError(f"Unknown resource: {uri}")
print("Resource handlers implemented")
def register_prompts(self):
"""Register prompt templates for clients."""
@self.server.list_prompts()
async def list_prompts() -> list[Prompt]:
"""
Return the list of available prompts.
This is called when clients want to discover what prompts are available.
"""
return [
Prompt(
name="analyze-data",
description="Analyze data stored in the server",
arguments=[
{
"name": "key",
"description": "The key of the data to analyze",
"required": True
}
]
),
Prompt(
name="calculate-scenario",
description="Walk through a calculation scenario",
arguments=[
{
"name": "operation",
"description": "The operation to demonstrate (add, subtract, multiply, divide)",
"required": True
}
]
)
]
print("Prompts registered: analyze-data, calculate-scenario")
def register_prompt_handlers(self):
"""Implement prompt generation logic."""
@self.server.get_prompt()
async def get_prompt(name: str, arguments: dict) -> GetPromptResult:
"""
Handle prompt generation requests.
This is called when a client wants to get a prompt.
"""
if name == "analyze-data":
key = arguments.get("key", "unknown")
value = self.data_store.get(key, "not found")
prompt_text = f"""Analyze the following data from the server:
Key: {key}
Value: {value}
Please provide:
1. A description of what this data represents
2. Any patterns or insights you notice
3. Suggestions for how this data could be used
Use the retrieve_data tool if you need to fetch additional context."""
return GetPromptResult(
messages=[
PromptMessage(
role="user",
content=TextContent(type="text", text=prompt_text)
)
]
)
elif name == "calculate-scenario":
operation = arguments.get("operation", "add")
prompt_text = f"""Let's work through a {operation} calculation scenario.
Use the calculate tool with the operation '{operation}'.
For example:
- Choose two numbers (a and b)
- Execute: calculate(operation="{operation}", a=10, b=5)
- Explain the result
This demonstrates how to use computational tools in the MCP server."""
return GetPromptResult(
messages=[
PromptMessage(
role="user",
content=TextContent(type="text", text=prompt_text)
)
]
)
else:
return GetPromptResult(
messages=[
PromptMessage(
role="user",
content=TextContent(
type="text",
text=f"Error: Unknown prompt '{name}'"
)
)
]
)
print("Prompt handlers implemented")
def setup_lifecycle_handlers(self):
"""Setup lifecycle management (conceptual for MCP)."""
print("Lifecycle management configured")
async def run(self):
"""Run the MCP server."""
# Initialize everything
self.register_tools()
self.register_tool_handlers()
self.register_resources()
self.register_resource_handlers()
self.register_prompts()
self.register_prompt_handlers()
self.setup_lifecycle_handlers()
# Connect to stdio
async with stdio_server() as (read_stream, write_stream):
print("Server running on stdio...")
await self.server.run(
read_stream,
write_stream,
self.server.create_initialization_options()
)
async def main():
"""Main entry point."""
server = CompleteMCPServer()
await server.run()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Server stopped by user")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
#!/usr/bin/env python3
"""
Complete MCP (Model Context Protocol) Server Implementation
Built step by step for learning purposes.
Step 13: Main Entry Point - Complete Orchestration
"""
import asyncio
import csv
import json
from typing import Any, Optional
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
Resource,
Prompt,
TextContent,
ImageContent,
EmbeddedResource,
GetPromptResult,
PromptMessage,
)
import sys
# Initialize an in-memory users
users = []
def load_users(csv_file_path: str):
"""Load users from a CSV file."""
global users
users = []
try:
with open(csv_file_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for i, row in enumerate(reader):
# Assuming CSV has 'content' and 'id' columns or similar
# Adjust column names as needed
content = row.get('content') or row.get('text') or list(row.values())[0]
doc_id = row.get('id') or f"doc_{i}"
if content:
users.append({"id": doc_id, "content": content})
print(f"Loaded {len(users)} documents into users.")
except Exception as e:
print(f"Error loading users: {e}")
# Load the users
# Make sure you have a 'users.csv' file in the same directory
# Format: id,content
load_users("users.csv")
class CompleteMCPServer:
"""
A comprehensive MCP Server implementation showcasing all protocol features.
This class demonstrates:
- Server initialization
- Tool registration and execution
- Resource management
- Prompt templates
- Request handling
- RAG capabilities
"""
def __init__(self):
"""Initialize the MCP Server instance."""
self.server = Server("complete-mcp-server")
self.data_store = {} # Simple in-memory data storage
print("Server instance created successfully!")
def register_tools(self):
"""Register all available tools with the MCP server."""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""
Return the list of available tools.
This is called when clients want to discover what tools are available.
"""
return [
Tool(
name="calculate",
description="Perform mathematical operations (add, subtract, multiply, divide)",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}
),
Tool(
name="store_data",
description="Store a key-value pair in the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to store"
},
"value": {
"type": "string",
"description": "The value to store"
}
},
"required": ["key", "value"]
}
),
Tool(
name="retrieve_data",
description="Retrieve a value from the server's data store",
inputSchema={
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to retrieve"
}
},
"required": ["key"]
}
),
Tool(
name="echo",
description="Echo back the input text",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to echo back"
}
},
"required": ["text"]
}
),
Tool(
name="filter_users_by_city",
description="Filter and return users who live in a specific city",
inputSchema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city to filter users by"
}
},
"required": ["city"]
}
),
Tool(
name="filter_users_by_age",
description="Filter and return users who are older than the specified minimum age",
inputSchema={
"type": "object",
"properties": {
"min_age": {
"type": "number",
"description": "The minimum age to filter users by"
}
},
"required": ["min_age"]
}
)
]
print("Tools registered: calculate, store_data, retrieve_data, echo, filter_users_by_city, filter_users_by_age")
def register_tool_handlers(self):
"""Implement the actual logic for each tool."""
@self.server.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""
Handle tool execution requests.
This is called when a client wants to execute a tool.
"""
if name == "calculate":
operation = arguments.get("operation")
a = arguments.get("a")
b = arguments.get("b")
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
return [TextContent(
type="text",
text="Error: Cannot divide by zero"
)]
result = a / b
else:
return [TextContent(
type="text",
text=f"Error: Unknown operation '{operation}'"
)]
return [TextContent(
type="text",
text=f"Result: {a} {operation} {b} = {result}"
)]
elif name == "store_data":
key = arguments.get("key")
value = arguments.get("value")
self.data_store[key] = value
return [TextContent(
type="text",
text=f"Stored: {key} = {value}"
)]
elif name == "retrieve_data":
key = arguments.get("key")
value = self.data_store.get(key)
if value is None:
return [TextContent(
type="text",
text=f"Error: Key '{key}' not found"
)]
return [TextContent(
type="text",
text=f"Retrieved: {key} = {value}"
)]
elif name == "echo":
text = arguments.get("text")
return [TextContent(
type="text",
text=f"Echo: {text}"
)]
elif name == "filter_users_by_city":
city = arguments.get("city", "")
filtered_users = []
target_city = city.lower().strip()
for user in users:
# Assuming user dict has 'city' key (loaded from CSV)
u_city = user.get("city", "").lower()
if u_city == target_city:
filtered_users.append(f"User {user.get('id')}: {user.get('content')} (City: {u_city})")
if not filtered_users:
result = f"No users found in {city}."
else:
result = "\n".join(filtered_users)
return [TextContent(type="text", text=result)]
elif name == "filter_users_by_age":
min_age = arguments.get("min_age", 0)
filtered_users = []
for user in users:
# Assuming user dict has 'age' (or 'value') key
u_age = user.get("age", user.get("value", 0))
try:
u_age = int(u_age)
except ValueError:
continue
if u_age > min_age:
filtered_users.append(f"User {user.get('id')}: {user.get('content')} (Age: {u_age})")
if not filtered_users:
result = f"No users found older than {min_age}."
else:
result = "\n".join(filtered_users)
return [TextContent(type="text", text=result)]
else:
return [TextContent(
type="text",
text=f"Error: Unknown tool '{name}'"
)]
print("Tool handlers implemented")
def register_resources(self):
"""Register resources that clients can access."""
@self.server.list_resources()
async def list_resources() -> list[Resource]:
"""
Return the list of available resources.
This is called when clients want to discover what resources are available.
"""
return [
Resource(
uri="resource://server-info",
name="Server Information",
description="Information about this MCP server",
mimeType="application/json"
),
Resource(
uri="resource://data-store",
name="Data Store",
description="Current contents of the data store",
mimeType="application/json"
),
Resource(
uri="resource://welcome",
name="Welcome Message",
description="Welcome message and server capabilities",
mimeType="text/plain"
)
]
print("Resources registered: server-info, data-store, welcome")
def register_resource_handlers(self):
"""Implement resource retrieval logic."""
@self.server.read_resource()
async def read_resource(uri: str) -> str:
"""
Handle resource read requests.
This is called when a client wants to read a resource.
"""
uri = str(uri) # Ensure uri is a string
if uri == "resource://server-info":
info = {
"name": "complete-mcp-server",
"version": "1.0.0",
"description": "A comprehensive MCP server implementation",
"capabilities": {
"tools": 6,
"resources": 3,
"prompts": 2
}
}
return json.dumps(info, indent=2)
elif uri == "resource://data-store":
return json.dumps(self.data_store, indent=2)
elif uri == "resource://welcome":
return """Welcome to the Complete MCP Server!
This server demonstrates all MCP protocol capabilities:
- Tools: Execute operations and computations
- Resources: Access data and information
- Prompts: Get structured prompt templates
- RAG: Retrieval Augmented Generation for user filtering
Explore the available tools and resources to see what this server can do."""
else:
raise ValueError(f"Unknown resource: {uri}")
print("Resource handlers implemented")
def register_prompts(self):
"""Register prompt templates for clients."""
@self.server.list_prompts()
async def list_prompts() -> list[Prompt]:
"""
Return the list of available prompts.
This is called when clients want to discover what prompts are available.
"""
return [
Prompt(
name="analyze-data",
description="Analyze data stored in the server",
arguments=[
{
"name": "key",
"description": "The key of the data to analyze",
"required": True
}
]
),
Prompt(
name="calculate-scenario",
description="Walk through a calculation scenario",
arguments=[
{
"name": "operation",
"description": "The operation to demonstrate (add, subtract, multiply, divide)",
"required": True
}
]
)
]
print("Prompts registered: analyze-data, calculate-scenario")
def register_prompt_handlers(self):
"""Implement prompt generation logic."""
@self.server.get_prompt()
async def get_prompt(name: str, arguments: dict) -> GetPromptResult:
"""
Handle prompt generation requests.
This is called when a client wants to get a prompt.
"""
if name == "analyze-data":
key = arguments.get("key", "unknown")
value = self.data_store.get(key, "not found")
prompt_text = f"""Analyze the following data from the server:
Key: {key}
Value: {value}
Please provide:
1. A description of what this data represents
2. Any patterns or insights you notice
3. Suggestions for how this data could be used
Use the retrieve_data tool if you need to fetch additional context."""
return GetPromptResult(
messages=[
PromptMessage(
role="user",
content=TextContent(type="text", text=prompt_text)
)
]
)
elif name == "calculate-scenario":
operation = arguments.get("operation", "add")
prompt_text = f"""Let's work through a {operation} calculation scenario.
Use the calculate tool with the operation '{operation}'.
For example:
- Choose two numbers (a and b)
- Execute: calculate(operation="{operation}", a=10, b=5)
- Explain the result
This demonstrates how to use computational tools in the MCP server."""
return GetPromptResult(
messages=[
PromptMessage(
role="user",
content=TextContent(type="text", text=prompt_text)
)
]
)
else:
return GetPromptResult(
messages=[
PromptMessage(
role="user",
content=TextContent(
type="text",
text=f"Error: Unknown prompt '{name}'"
)
)
]
)
print("Prompt handlers implemented")
def setup_lifecycle_handlers(self):
"""Setup lifecycle management (conceptual for MCP)."""
print("Lifecycle management configured")
async def run(self):
"""Start the MCP server and begin serving requests."""
print("Starting MCP server...")
print("Server is now running and ready to accept connections")
async with stdio_server() as (read_stream, write_stream):
await self.server.run(
read_stream,
write_stream,
self.server.create_initialization_options()
)
async def main():
"""
Main entry point for the MCP server.
This function orchestrates the complete server setup and execution:
1. Creates server instance (constructor)
2. Registers tools
3. Registers tool handlers
4. Registers resources
5. Registers resource handlers
6. Registers prompts
7. Registers prompt handlers
8. Sets up lifecycle handlers
9. Runs the server
"""
print("="*80)
print("🌟 COMPLETE MCP SERVER - STARTING")
print("="*80)
# Step 1: Create server instance
server = CompleteMCPServer()
# Step 2: Register tools
server.register_tools()
# Step 3: Register tool handlers
server.register_tool_handlers()
# Step 4: Register resources
server.register_resources()
# Step 5: Register resource handlers
server.register_resource_handlers()
# Step 6: Register prompts
server.register_prompts()
# Step 7: Register prompt handlers
server.register_prompt_handlers()
# Step 8: Setup lifecycle handlers
server.setup_lifecycle_handlers()
print("="*80)
print("All components registered successfully!")
print("="*80)
# Step 9: Run the server
await server.run()
if __name__ == "__main__":
"""
Entry point when script is run directly.
This runs when you execute: python mcp_server.py
"""
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n👋 Server shutdown complete")
sys.exit(0)
#!/bin/bash
# Determine the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$SCRIPT_DIR" # Run from the same directory
# Define the server script path
SERVER_SCRIPT="${SCRIPT_DIR}/mcp_server.py"
# Navigate to the project root directory
cd "$PROJECT_ROOT"
# Check if the virtual environment exists
if [ -d ".venv" ]; then
echo "Activating virtual environment..."
source .venv/bin/activate
else
echo "Creating virtual environment..."
python3 -m venv .venv
source .venv/bin/activate
echo "Installing requirements..."
pip install -r requirements.txt
fi
if [ ! -f "$SERVER_SCRIPT" ]; then
echo "Error: Server file $SERVER_SCRIPT not found"
exit 1
fi
echo "Starting MCP Inspector with server..."
echo "URL should open in your browser shortly..."
echo "Press Ctrl+C to stop."
# Use the python executable from the virtual environment
PYTHON_EXEC="$PROJECT_ROOT/.venv/bin/python3"
npx @modelcontextprotocol/inspector "$PYTHON_EXEC" "$SERVER_SCRIPT"